Rodhos Soft

備忘録を兼ねた技術的なメモです。Rofhos SoftではiOSアプリ開発を中心としてAndroid, Webサービス等の開発を承っております。まずはご相談下さい。

簡単な画面遷移

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に渡せていなくて ローカルファイルの読み込みができていない。 そのところは変える必要があるだろう。