簡単な画面遷移
WebからJSON取得して表示するリスト画面と設定画面の遷移をするっでもを作ってみた。 設定画面でwebから取得するかローカルファイルから取得するか選べる。
まずモデルは
public struct Article: Codable { let title:String let url:String }
という簡単なもの。
ローカルファイルにはJSON形式で
[{"title":"[pre]【Unity】FungusをLuaで使用する方法","url":"https:\/\/qiita.com\/Humimaro\/items\/76e1730dde5c359f61ba"},...]
のようなものを準備しておく。
ファイルのロードは
import Foundation import Combine func load<T:Decodable>(_ filename:String) -> AnyPublisher<T, Error> { let data:Data guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else { return Fail(error: NSError(domain: "FILE", code: 1, userInfo: nil)).eraseToAnyPublisher() } do { data = try Data(contentsOf: file) } catch { return Fail(error: NSError(domain: "FILE_DATA", code: 1, userInfo: nil)).eraseToAnyPublisher() } do { let decoder = JSONDecoder() let obj = try decoder.decode(T.self, from: data) return Just(obj).setFailureType(to: Error.self).eraseToAnyPublisher() } catch { return Fail(error: error).eraseToAnyPublisher() } }
のように、Combineを利用してみた。
Webから取得するところは
import Combine import Foundation var cancellables = [AnyCancellable]() // 記事を取得する。 func fetchArticles(isPreview:Bool) -> AnyPublisher<[Article], Error> { if isPreview { return load("articles.json") } let url = URL(string: "https://qiita.com/api/v2/items")! let request = URLRequest(url:url) return URLSession.shared .dataTaskPublisher(for: request) .tryMap({ (data, response) -> Data in guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw URLError(URLError.Code.badServerResponse) } return data }) .decode(type: [Article].self, decoder: JSONDecoder()) .eraseToAnyPublisher() }
のように書き、引数によって、ファイルから取得できるようにしてみた。
ViewModelは
import Foundation import SwiftUI import Combine // 記事ビューモデル class ViewModel : ObservableObject { @Published private(set) var text: String = "Hello, World!" @Published private(set) var articles:[Article] = [] let previewFlg:Bool var cancels = [AnyCancellable]() init(isPreview:Bool = false) { previewFlg = isPreview } // タップした func onTapped() { // 記事を取得する fetchArticles(isPreview: previewFlg) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { result in switch result { case .failure(let error): self.text = error.localizedDescription case .finished: break } }) { articles in self.articles = articles }.store(in: &cancels) } }
のように、タップしたらフェッチしてそれをarticlesに入れるという動きを書いた。
これに対応するContentViewを
import SwiftUI import Combine struct ContentView: View { @ObservedObject var viewModel:ViewModel @EnvironmentObject var settingModel:SettingModel var body: some View { VStack { Toggle(isOn:$settingModel.testMode) { Text("") } // if self.settingModel.testMode { // Text("testMode") // } Button(action: { self.viewModel.onTapped() }) { Text("更新") } List(viewModel.articles, id: \.title) { article in HStack { Text(article.title) Spacer() Button("open") { UIApplication.shared.open( URL(string: article.url)! ) } } } Text(viewModel.text) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView(viewModel: ViewModel(isPreview: true)).environmentObject(SettingModel()) } }
のようにした。リスト表示の部分が先程のarticlesの表示をする。 設定からの情報を表示するためにenvironmentObjectも使っている。
設定画面は
import SwiftUI struct SettingView: View { @EnvironmentObject var settingModel:SettingModel var body: some View { VStack { HStack { Text("TestMode") Spacer() Toggle(isOn: $settingModel.testMode) { Text(settingModel.testMode ? "":"") } }.padding() } } } struct Setting_Previews: PreviewProvider { static var previews: some View { SettingView().environmentObject(SettingModel()) } }
という単純なもの、settingModelはenvironmentObjectでSceneDelegateで注入されていて
import Foundation import Combine import SwiftUI // 設定モデル class SettingModel : ObservableObject { @Published var testMode: Bool = false }
BaseViewはこの2つをとりまとめていて、
import SwiftUI struct BaseView: View { @State var setting:Bool = false var body: some View { NavigationView { VStack { Button("設定") { self.setting = true print("x") } ContentView(viewModel: ViewModel()) NavigationLink(destination: SettingView(), isActive: $setting) { EmptyView() } } }.navigationViewStyle(StackNavigationViewStyle()) } } struct BaseView_Previews: PreviewProvider { static var previews: some View { BaseView().environmentObject(SettingModel()) } }
のようにNavigationLinkで設定画面に移動できるようにしている。
これで実装できたが、実際は設定画面の情報をContentのViewModelに渡せていなくて ローカルファイルの読み込みができていない。 そのところは変える必要があるだろう。