1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SwiftUIのプロジェクトで作ったViewModelの値を独自のUIViewからもやり取りしたい

Last updated at Posted at 2022-11-09

SwiftUIで作っているプロジェクトの中に作ったUIView(UIViewRepresentable経由)で親ViewのViewModelを更新したい

履歴

2022年11月9日 初版
2022年11月9日 履歴にかかれていた初版の日付修正と、Githubへのリンク追加

開発環境

XCode Version 14.0 (14A309)
Swift version 5.7

準備

確認用CustomUIView

CustomUIViewという名前で以下のXibファイルを作る
制約は適当
image.png

CustomClassは次のようにする。

import UIKit

class CustomUIView: UIView {
    
    @IBOutlet private weak var messageLabel: UILabel! 
    var label: String {
        get {
            messageLabel?.text ?? ""
        }
        set {
            messageLabel?.text = newValue
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }
    
    func loadNib() {
        let nibName = String(describing: type(of: self))
        let bundle = Bundle(for: type(of: self))
        if let view = bundle.loadNibNamed(nibName, owner: self, options: nil)?.first as? UIView {
            view.frame = self.bounds
            self.addSubview(view)
        }
    }
    
    @IBAction private func tappedChangeMessage(_ sender: UIButton) {
        let formatter = DateFormatter()
        formatter.timeStyle = .medium
        formatter.dateStyle = .medium
        formatter.locale = Locale(identifier: "ja_JP")
        let now = Date()
        
        label = formatter.string(from: now)
    }
    
}

確認用UIViewRepresentable

struct CustomView: UIViewRepresentable {
    typealias UIViewType = CustomUIView
    
    func makeUIView(context: Context) -> CustomUIView {
        let customView = CustomUIView()
        return customView
    }
    
    func updateUIView(_ uiView: CustomUIView, context: Context) {
        // 動作無し
    }
}

確認用ContentView

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            CustomView()
                .frame(height: 100)
        }
    }
}

ここまでの動作を確認する

画面上に「Label」と表示されて、「時間」を押すと時間が表示される

SwiftUI部分とViewModelを作る

// ViewModel
class ContentViewModel: ObservableObject {
    @Published var displayingLabel: String = "hogeらっちょ"
}

ContentViewを全体的に修正する

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Spacer()
                Text("SwiftUI(のViewModelを変える)↓")
                Spacer()
            }
            HStack {
                Text(viewModel.displayingLabel)
                    .padding(.horizontal, 10)
                Spacer()
                Button {
                    viewModel.displayingLabel = "きえた"
                } label: {
                    Text("くりあ")
                        .frame(width: 75, height: 60)
                        .background(.blue)
                        .foregroundColor(Color.white)
                        .padding(.trailing, 20)
                }
            }
            
            HStack {
                Spacer()
                Text("UIViewからViewModelを変える↓")
                Spacer()
            }
            CustomView()
                .frame(height: 100)
        }
        .padding()
    }
}

image.png

「くりあ」を押すと「hogeらっちょ」は「きえた」に変わる

image.png

UIViewの「時間」ボタンを押したら「hogeらっちょ」の値を変える

UIViewの値を変更したらViewModelの値を変更する

CustomViewにViewModelを渡すようにする

struct CustomView: UIViewRepresentable {
    typealias UIViewType = CustomUIView
    
+   @ObservedObject var viewModel: ContentViewModel
    
    func makeUIView(context: Context) -> CustomUIView {
        let customView = CustomUIView()
+        customView.viewModel = viewModel
        return customView
    }
    
    func updateUIView(_ uiView: CustomUIView, context: Context) {
    }
}

ContentViewもViewModelを渡すようにする

struct ContentView: View {
    @StateObject var viewModel = ContentViewModel()
    
    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Spacer()
                Text("SwiftUI(のViewModelを変える)↓")
                Spacer()
            }
            HStack {
                Text(viewModel.displayingLabel)
                    .padding(.horizontal, 10)
                Spacer()
                Button {
                    viewModel.displayingLabel = "きえた"
                } label: {
                    Text("くりあ")
                        .frame(width: 75, height: 60)
                        .background(.blue)
                        .foregroundColor(Color.white)
                        .padding(.trailing, 20)
                }
            }
            
            HStack {
                Spacer()
                Text("UIViewからViewModelを変える↓")
                Spacer()
            }
-            CustomView()
+            CustomView(viewModel: viewModel)
                .frame(height: 100)
        }
        .padding()
    }
}

CustomUIViewもViewModelを受け取れるようにする

import UIKit

class CustomUIView: UIView {
    
    @IBOutlet private weak var messageLabel: UILabel!
    
+    var viewModel: ContentViewModel?
    
    var label: String {
        get {
            messageLabel?.text ?? ""
        }
        set {
            messageLabel?.text = newValue
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }
    
    func loadNib() {
        let nibName = String(describing: type(of: self))
        let bundle = Bundle(for: type(of: self))
        if let view = bundle.loadNibNamed(nibName, owner: self, options: nil)?.first as? UIView {
            view.frame = self.bounds
            self.addSubview(view)
        }
    }
    
    @IBAction private func tappedChangeMessage(_ sender: UIButton) {
        let formatter = DateFormatter()
        
        formatter.timeStyle = .medium
        formatter.dateStyle = .medium
        formatter.locale = Locale(identifier: "ja_JP")
        let now = Date()
        
        label = formatter.string(from: now)
+        // viewModelの値を変更する
+        guard let viewModel else { return }
+        viewModel.displayingLabel = formatter.string(from: now)
    }
    
}

確認すると、時間を押すとHogeらっちょは変わることがわかる
ただ、「くりあ」を押してもUIView側の値は変わらない

Screenshot_2022-11-09_at_13_53_18_AdobeExpress.gif

SwiftUI側からUIKit側の値を更新する

方法

  1. updateUIViewメソッドを使う
  2. Combineを使う

updateUIViewメソッドを使う方法

これはかなり簡単
updateUIViewは画面描画のタイミングで呼ばれる
SwiftUI側で再描画が起きた時に自動で呼ばれるので修正が1行で足りる

struct CustomView: UIViewRepresentable {
    typealias UIViewType = CustomUIView
    @ObservedObject var viewModel: ContentViewModel
    
    func makeUIView(context: Context) -> CustomUIView {
        let customView = CustomUIView()
        customView.viewModel = viewModel
        return customView
    }
    
    func updateUIView(_ uiView: CustomUIView, context: Context) {
+        uiView.label = viewModel.displayingLabel
    }
}

Screenshot_2022-11-09_at_14_06_03_AdobeExpress.gif

Combineを使う

Combine大好き! って人でもあまり採用しなさそうな対応

import UIKit
+import Combine

class CustomUIView: UIView {
    
    @IBOutlet private weak var messageLabel: UILabel!
+    private var cancellables = Set<AnyCancellable>()
 
-    var viewModel: ContentViewModel?    
+    var viewModel: ContentViewModel? {
+        didSet {
+            if oldValue !== viewModel {
+                guard let viewModel else { return }
+                viewModel.$displayingLabel
+                    .map { Optional($0) }
+                    .receive(on: DispatchQueue.main)
+                    .assign(to: \UILabel.text, on: messageLabel)
+                    .store(in: &cancellables)
+            }
+        }
+    }
    
-    var label: String {
-        get {
-            messageLabel?.text ?? ""
-        }
-        set {
-            messageLabel?.text = newValue
-        }
-    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        loadNib()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        loadNib()
    }
    
    func loadNib() {
        let nibName = String(describing: type(of: self))
        let bundle = Bundle(for: type(of: self))
        if let view = bundle.loadNibNamed(nibName, owner: self, options: nil)?.first as? UIView {
            view.frame = self.bounds
            self.addSubview(view)
        }
    }
    
    @IBAction private func tappedChangeMessage(_ sender: UIButton) {
        let formatter = DateFormatter()
        
        formatter.timeStyle = .medium
        formatter.dateStyle = .medium
        formatter.locale = Locale(identifier: "ja_JP")
        let now = Date()
        
-        label = formatter.string(from: now)
        guard let viewModel else { return }
        viewModel.displayingLabel = formatter.string(from: now)
    }
    
}

label = formatter.string(from: now)消してしまうから、viewModelがnilの時困るけれども、
その場合は設計ミスということで。

Screenshot_2022-11-09_at_14_14_29_AdobeExpress.gif

参考用

今回使ったソースが置かれているGithubへのリンク

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?