web-dev-qa-db-ja.com

Combine + SwiftUIでのデータバインディングのベストプラクティスは?

RxSwiftでは、_View Model_のDriverまたはObservableViewController(つまり、UILabel)のオブザーバーにバインドするのは非常に簡単です。

私は通常、「PublishSubject」を介して「強制的に」値をプッシュするのではなく、オブザーバブル他のオブザーバブルから作成を使用してパイプラインを構築することを好みます。

この例を使用してみましょう:ネットワークからデータをフェッチした後、UILabelを更新します


RxSwift + RxCocoaの例

_final class RxViewModel {
    private var dataObservable: Observable<Data>

    let stringDriver: Driver<String>

    init() {
        let request = URLRequest(url: URL(string:"https://www.google.com")!)

        self.dataObservable = URLSession.shared
            .rx.data(request: request).asObservable()

        self.stringDriver = dataObservable
            .asDriver(onErrorJustReturn: Data())
            .map { _ in return "Network data received!" }
    }
}
_
_final class RxViewController: UIViewController {
    private let disposeBag = DisposeBag()
    let rxViewModel = RxViewModel()

    @IBOutlet weak var rxLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        rxViewModel.stringDriver.drive(rxLabel.rx.text).disposed(by: disposeBag)
    }
}
_

組み合わせ+ UIKitの例

UIKitベースのプロジェクトでは、同じパターンを維持できるようです。

  • ビューモデルはパブリッシャーを公開します
  • ビューコントローラーは、UI要素をそれらのパブリッシャーにバインドします
_final class CombineViewModel: ObservableObject {
    private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
    var stringPublisher: AnyPublisher<String, Never>

    init() {
        self.dataPublisher = URLSession.shared
            .dataTaskPublisher(for: URL(string: "https://www.google.it")!)
            .eraseToAnyPublisher()

        self.stringPublisher = dataPublisher
            .map { (_, _) in return "Network data received!" }
            .replaceError(with: "Oh no, error!")
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}
_
_final class CombineViewController: UIViewController {
    private var cancellableBag = Set<AnyCancellable>()
    let combineViewModel = CombineViewModel()

    @IBOutlet weak var label: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        combineViewModel.stringPublisher
            .flatMap { Just($0) }
            .assign(to: \.text, on: self.label)
            .store(in: &cancellableBag)
    }
}
_

SwiftUIはどうですか?

SwiftUIは、_@Published_などのプロパティラッパーとObservableObjectObservedObjectなどのプロトコルを使用して、バインディングを自動的に処理します(Xcode 11b7以降)。

(AFAIK)プロパティラッパーは「オンザフライで作成」できないため、同じパターンを使用して上記の例を再作成する方法はありません。次のコンパイルしません

_final class WrongViewModel: ObservableObject {
    private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>
    @Published var stringValue: String

    init() {
        self.dataPublisher = URLSession.shared
            .dataTaskPublisher(for: URL(string: "https://www.google.it")!)
            .eraseToAnyPublisher()

        self.stringValue = dataPublisher.map { ... }. ??? <--- WRONG!
    }
}
_

私が思いつく可能性がある最も近いのはビューモデルでサブスクライブする(UGH!)プロパティを強制的に更新するです。

_final class SwiftUIViewModel: ObservableObject {
    private var cancellableBag = Set<AnyCancellable>()
    private var dataPublisher: AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>

    @Published var stringValue: String = ""

    init() {
        self.dataPublisher = URLSession.shared
            .dataTaskPublisher(for: URL(string: "https://www.google.it")!)
            .eraseToAnyPublisher()

        dataPublisher
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: {_ in }) { (_, _) in
            self.stringValue = "Network data received!"
        }.store(in: &cancellableBag)
    }
}
_
_struct ContentView: View {
    @ObservedObject var viewModel = SwiftUIViewModel()

    var body: some View {
        Text(viewModel.stringValue)
    }
}
_

この新しいIViewController-lessの世界では、「バインディングを行うための古い方法」は忘れられて置き換えられますか?

16
Enrico Querci

以前の回答を投稿した後、この記事を読んでください: https://nalexn.github.io/swiftui-observableobject/

同じようにすることにします。 @Stateを使用し、@ Publishedは使用しない

一般的なViewModelプロトコル:

protocol ViewModelProtocol {
    associatedtype Output
    associatedtype Input

    func bind(_ input: Input) -> Output
}

ViewModelクラス:

final class SwiftUIViewModel: ViewModelProtocol {
    struct Output {
        var dataPublisher: AnyPublisher<String, Never>
    }

    typealias Input = Void

    func bind(_ input: Void) -> Output {
        let dataPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
        .map{ "Just for testing - \($0)"}
        .replaceError(with: "An error occurred")
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()

        return Output(dataPublisher: dataPublisher)
    }
}

SwiftUIビュー:

struct ContentView: View {

    @State private var dataPublisher: String = "ggg"

    let viewModel: SwiftUIViewModel
    let output: SwiftUIViewModel.Output

    init(viewModel: SwiftUIViewModel) {
        self.viewModel = viewModel
        self.output = viewModel.bind(())
    }

    var body: some View {
        VStack {
            Text(self.dataPublisher)
        }
        .onReceive(output.dataPublisher) { value in
            self.dataPublisher = value
        }
    }
}

0

私は多少の妥協に終わった。 viewModelで@Publishedを使用していますが、SwiftUI Viewでサブスクライブしています。このようなもの:

final class SwiftUIViewModel: ObservableObject {
    struct Output {
        var dataPublisher: AnyPublisher<String, Never>
    }

    @Published var dataPublisher : String = "ggg"

    func bind() -> Output {
        let dataPublisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://www.google.it")!)
        .map{ "Just for testing - \($0)"}
        .replaceError(with: "An error occurred")
        .receive(on: DispatchQueue.main)
        .eraseToAnyPublisher()

        return Output(dataPublisher: dataPublisher)
    }
}

およびSwiftUI:

struct ContentView: View {
    private var cancellableBag = Set<AnyCancellable>()

    @ObservedObject var viewModel: SwiftUIViewModel

    init(viewModel: SwiftUIViewModel) {
        self.viewModel = viewModel

        let bindStruct = viewModel.bind()
        bindStruct.dataPublisher
            .assign(to: \.dataPublisher, on: viewModel)
            .store(in: &cancellableBag)
    }

    var body: some View {
        VStack {
            Text(self.viewModel.dataPublisher)
        }
    }
}

0