Last active
November 28, 2020 17:13
-
-
Save lamprosg/255008560dad2fb3a21ab99352751805 to your computer and use it in GitHub Desktop.
(iOS) SwiftUI reusable views
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Content loading is at least three stage process: | |
- Before the loading starts there is the initial moment. | |
- The actual process of loading content. We need to present some progress or activity indication in the UI; | |
- And finally the result, success or failure. | |
SwiftUI encourages creating small, reusable views, and use composition to create the complete picture. | |
Each stage of the content loading process will require a view. The container view will compose the result. | |
*/ | |
//Full documentation: | |
//https://medium.com/flawless-app-stories/building-reusable-content-loading-view-with-swiftui-and-combine-f4886fe77e2b |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//The loading state | |
enum RemoteContentLoadingState<Value> { | |
case initial | |
case inProgress | |
case success(_ value: Value) | |
case failure(_ error: Error) | |
} | |
//Since SwiftUI views are value types, content loading requires back and forth communication only with reference types. | |
//Because we are building a reusable view it makes sense to inject ObservableObject. | |
protocol RemoteContent : ObservableObject { | |
associatedtype Value | |
var loadingState: RemoteContentLoadingState<Value> { get } | |
func load() | |
func cancel() | |
} | |
//Since there is an associatedtype we need a type erasure object to create RemoteContent freely. | |
final class AnyRemoteContent<Value> : RemoteContent { | |
private let loadingStateClosure: () -> RemoteContentLoadingState<Value> | |
private let loadClosure: () -> Void | |
private let cancelClosure: () -> Void | |
//The Observed object publisher | |
private let objectWillChangeClosure: () -> ObjectWillChangePublisher | |
init<R: RemoteContent>(_ remoteContent: R) where R.ObjectWillChangePublisher == ObjectWillChangePublisher, | |
R.Value == Value { | |
objectWillChangeClosure = { | |
remoteContent.objectWillChange | |
} | |
loadingStateClosure = { | |
remoteContent.loadingState | |
} | |
loadClosure = { | |
remoteContent.load() | |
} | |
cancelClosure = { | |
remoteContent.cancel() | |
} | |
} | |
//ObservableObject protocol synthesizes a publisher that emits before the object has changed | |
var objectWillChange: ObservableObjectPublisher { | |
objectWillChangeClosure() | |
} | |
var loadingState: RemoteContentLoadingState<Value> { | |
loadingStateClosure() | |
} | |
func load() { | |
loadClosure() | |
} | |
func cancel() { | |
cancelClosure() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct RemoteContentView<Value, Empty, Progress, Failure, Content> : View where Empty : View, | |
Progress : View, | |
Failure : View, | |
Content : View | |
{ | |
let empty: () -> Empty | |
let progress: () -> Progress | |
let failure: (_ error: Error, _ retry: @escaping () -> Void) -> Failure | |
let content: (_ value: Value) -> Content | |
init<R: RemoteContent>(remoteContent: R, | |
empty: @escaping () -> Empty, | |
progress: @escaping () -> Progress, | |
failure: @escaping (_ error: Error, _ retry: @escaping () -> Void) -> Failure, | |
content: @escaping (_ value: Value) -> Content) where R.ObjectWillChangePublisher == ObservableObjectPublisher, | |
R.Value == Value | |
{ | |
self.remoteContent = AnyRemoteContent(remoteContent) | |
self.empty = empty | |
self.progress = progress | |
self.failure = failure | |
self.content = content | |
} | |
var body: some View { | |
@ObservedObject private var remoteContent: AnyRemoteContent<Value> | |
ZStack { | |
switch remoteContent.loadingState { | |
case .initial: | |
empty() | |
case .inProgress: | |
progress() | |
case .success(let value): | |
content(value) | |
case .failure(let error): | |
failure(error) { | |
remoteContent.load() | |
} | |
} | |
} | |
.onAppear { | |
remoteContent.load() | |
} | |
.onDisappear { | |
remoteContent.cancel() | |
} | |
} | |
} | |
extension RemoteContentView where Empty == EmptyView, Progress == ActivityIndicator, Failure == Text { | |
init<R: RemoteContent>(remoteContent: R, | |
content: @escaping (_ value: Value) -> Content) where R.ObjectWillChangePublisher == ObservableObjectPublisher, | |
R.Value == Value | |
{ | |
self.init(remoteContent: remoteContent, | |
empty: { EmptyView() }, | |
progress: { ActivityIndicator() }, | |
failure: { error, _ in Text(error.localizedDescription) }, | |
content: content) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import RemoteContentView | |
//The remote content | |
final class DecodableRemoteContent<Value> : RemoteContent | |
{ | |
@Published private(set) var loadingState: RemoteContentLoadingState<Value> = .initial | |
func load() { | |
// Set state to in progress | |
loadingState = .inProgress | |
//... | |
//Do something, start loading code | |
//.. | |
//Loading finished and got a value | |
loadingState.success(someValue) // Or .failure(someError) | |
} | |
func cancel() { | |
// Reset loading state | |
loadingState = .initial | |
//... | |
// Stop loading code | |
} | |
} | |
struct Post : Codable { | |
var id: Int | |
// ... | |
} | |
struct PostView : View { | |
// ... | |
} | |
struct PostsView : View { | |
var body: some View { | |
let content = DecodableRemoteContent() //Or a custom initiaizer to do something | |
return RemoteContentView(remoteContent: content) { | |
List($0, id: \Post.id) { | |
PostView(post: $0) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment