Skip to content

Instantly share code, notes, and snippets.

@lamprosg
Last active November 28, 2020 17:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lamprosg/255008560dad2fb3a21ab99352751805 to your computer and use it in GitHub Desktop.
Save lamprosg/255008560dad2fb3a21ab99352751805 to your computer and use it in GitHub Desktop.
(iOS) SwiftUI reusable views
/*
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
//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()
}
}
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)
}
}
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