GitHub Combine

Resources

Repositories

Books

Username: vaskaloidis
Password: Flyp2021!!!

Misc

Code Notes

  // Combine MVVM https://github.com/V8tr/ModernMVVM  
  // https://github.com/V8tr/ModernMVVM/tree/master/ModernMVVM/Features/MovieDetails

  import Foundation
  import Combine

  final class MovieDetailViewModel: ObservableObject {
      @Published private(set) var state: State

      private var bag = Set<AnyCancellable>()

      private let input = PassthroughSubject<Event, Never>()

      init(movieID: Int) {
          state = .idle(movieID)

          Publishers.system(
              initial: state,
              reduce: Self.reduce,
              scheduler: RunLoop.main,
              feedbacks: [
                  Self.whenLoading(),
                  Self.userInput(input: input.eraseToAnyPublisher())
              ]
          )
          .assign(to: \.state, on: self)
          .store(in: &bag)
      }

      func send(event: Event) {
          input.send(event)
      }
  }

  // MARK: - Inner Types
  extension MovieDetailViewModel {
      enum State {
          case idle(Int)
          case loading(Int)
          case loaded(MovieDetail)
          case error(Error)
      }

      enum Event {
          case onAppear
          case onLoaded(MovieDetail)
          case onFailedToLoad(Error)
      }

      struct MovieDetail {
          let id: Int
          let title: String
          let overview: String?
          let poster: URL?
          let rating: Double?
          let duration: String
          let genres: [String]
          let releasedAt: String
          let language: String

          init(movie: MovieDetailDTO) {
              id = movie.id
              title = movie.title
              overview = movie.overview
              poster = movie.poster
              rating = movie.vote_average

              let formatter = DateComponentsFormatter()
              formatter.unitsStyle = .abbreviated
              formatter.allowedUnits = [.minute, .hour]
              duration = movie.runtime.flatMap { formatter.string(from: TimeInterval($0 * 60)) } ?? "N/A"

              genres = movie.genres.map(\.name)

              releasedAt = movie.release_date ?? "N/A"

              language = movie.spoken_languages.first?.name ?? "N/A"
          }
      }
  }

  // MARK: - State Machine
  extension MovieDetailViewModel {
      static func reduce(_ state: State, _ event: Event) -> State {
          switch state {
          case .idle(let id):
              switch event {
              case .onAppear:
                  return .loading(id)
              default:
                  return state
              }
          case .loading:
              switch event {
              case .onFailedToLoad(let error):
                  return .error(error)
              case .onLoaded(let movie):
                  return .loaded(movie)
              default:
                  return state
              }
          case .loaded:
              return state
          case .error:
              return state
          }
      }

      static func whenLoading() -> Feedback<State, Event> {
          Feedback { (state: State) -> AnyPublisher<Event, Never> in
              guard case .loading(let id) = state else { return Empty().eraseToAnyPublisher() }
              return MoviesAPI.movieDetail(id: id)
                  .map(MovieDetail.init)
                  .map(Event.onLoaded)
                  .catch { Just(Event.onFailedToLoad($0)) }
                  .eraseToAnyPublisher()
          }
      }

      static func userInput(input: AnyPublisher<Event, Never>) -> Feedback<State, Event> {
          Feedback(run: { _ in
              return input
          })
      }
  }
  // filter() vs. compactMap()

  // https://cocoacasts.com/collections/combine-essentials

  private func setupBindings() {
      reachablePublisher
          .filter { $0 }
          .sink { [weak self] _ in
              self?.fetchEpisodes()
          }.store(in: &subscriptions)
  }

  private func setupBindings() {
      reachablePublisher
          .compactMap { $0 ? $0 : nil }
          .sink { [weak self] _ in
              self?.fetchEpisodes()
          }.store(in: &subscriptions)
  }
  // AnyCancellable

  var mySubscriber: AnyCancellable?

  let mySinkSubscriber = remotePublisher
      .sink { data in
          print("received ", data)
      }
  mySubscriber = AnyCancellable(mySinkSubscriber)

  // Alternatively

  private var cancellableSet: Set<AnyCancellable> = []

  let mySinkSubscriber = remotePublisher
      .sink { data in
          print("received ", data)
      }
      .store(in: &cancellableSet)
// SwiftUI Published Binding

struct ReactiveForm: View {

    @ObservedObject var model: ReactiveFormModel
    // $model is a ObservedObject<ExampleModel>.Wrapper
    // and $model.objectWillChange is a Binding<ObservableObjectPublisher>
    @State private var buttonIsDisabled = true
    // $buttonIsDisabled is a Binding<Bool>
    var body: some View {
        VStack {
            Text("Reactive Form")
                .font(.headline)

            Form {
                TextField("first entry", text: $model.firstEntry)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .lineLimit(1)
                    .multilineTextAlignment(.center)
                    .padding()

                TextField("second entry", text: $model.secondEntry)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .multilineTextAlignment(.center)
                    .padding()

                VStack {
                    ForEach(model.validationMessages, id: \.self) { msg in
                        Text(msg)
                            .foregroundColor(.red)
                            .font(.callout)
                    }
                }
            }

            Button(action: {}) {
                Text("Submit")
            }.disabled(buttonIsDisabled)
                .onReceive(model.submitAllowed) { submitAllowed in
                    self.buttonIsDisabled = !submitAllowed
            }
            .padding()
            .background(RoundedRectangle(cornerRadius: 10)      .stroke(Color.blue, lineWidth: 1)
            )


            Spacer()
        }
    }
}

struct HeadingView: View {
    @ObservedObject var locationModel: LocationProxy
    @State var lastHeading: CLHeading?
    @State var lastLocation: CLLocation?

    var body: some View {
        VStack {
            HStack {
                Text("authorization status:")
                Text(locationModel.authorizationStatusString())
            }
            if (locationModel.authorizationStatus == .notDetermined) {
                Button(action: {
                    self.locationModel.requestAuthorization()
                }) {
                    Image(systemName: "lock.shield")
                    Text("Request location authorization")
                }
                .padding()
                .background(RoundedRectangle(cornerRadius: 10)      .stroke(Color.blue, lineWidth: 1)
                )
            }
            if (self.lastHeading != nil) {
                Text("Heading: ")+Text(String(self.lastHeading!.description))
            }
            if (self.lastLocation != nil) {
                Text("Location: ")+Text(lastLocation!.description)
                ZStack {
                    Circle()
                        .stroke(Color.blue, lineWidth: 1)

                    GeometryReader { geometry in
                        Path { path in
                            let minWidthHeight = min(geometry.size.height, geometry.size.width)

                            path.move(to: CGPoint(x: geometry.size.width/2, y: geometry.size.height/2))
                            path.addLine(to: CGPoint(x: geometry.size.width/2, y: geometry.size.height/2 - minWidthHeight/2 + 5)  )
                        }
                        .stroke()
                        .rotation(Angle(degrees: self.lastLocation!.course))
                        .animation(.linear)
                    }
                }

            }
        }
        .onReceive(self.locationModel.headingPublisher) { heading in
            self.lastHeading = heading
        }
        .onReceive(self.locationModel.locationPublisher, perform: {
            self.lastLocation = $0
        })

    }
}

struct ContentView : View {
    @ObservedObject var model: ReactiveFormModel

    var body: some View {
        TabView {
            ReactiveForm(model: model)
            .tabItem {
                Image(systemName: "1.circle")
                Text("Reactive Form")
            }

            HeadingView(locationModel: LocationProxy())
            .tabItem {
                Image(systemName: "mappin.circle")
                Text("Location")
            }
        }
    }
}
// https://heckj.github.io/swiftui-notes/#reference-swiftui

// SwiftUI Binding

class Contact : ObservableObject {
    @Published var name: String
    @Published var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    func haveBirthday() -> Int {
        age += 1
        return age
    }
 }

 let john = Contact(name: "John Appleseed", age: 24)
 let cancellable = john.objectWillChange.sink { _ in
    expectation.fulfill()
    print("will change")
    // Prints "will change"
    // Prints "25"
}
print(john.haveBirthday())