Rodhos Soft

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

構造体を文字列としてみる

struct Hoge {
 let name:String
 let age:Int
}

extension String.StringInterpolation {
mutationg func appendInterpolation(_ value: User) {
  appendInterpolation("Hoge's name is \(value.name) and he name is \(value.age)")
 }
}

としておいて、

let hoge = Hoge(name:"poi", age:44)
print("Hoge deatil \(hoge)) /// ここでhogeを引数にとっていることがポイント

これはCustomStringConvertibleと同じである。

しかし、こんなこともできる・

引数を好きなだけとれるのだ

extension String.StringIterpolation {
mutating func appendInterpolation(_ number: Int, style:NumberFormatter.Style) {
 formatter.numberStyle = style
 if let result = formatter.string(from: number as NSNumber) {
  appendLiteral(result)
 }
}

これで $20とか、1st, 12thとかできる。

let number = Int.random(in:0...100)
let lucky = "lucky number this weeks os \(number, style: .spellOut)."
print(lucky)

appendLiteralは必要に応じて何度も呼べる。

extension String.StringInterpolation {
mutating func appendInterpolation(repeat str: string, _ count:Int) {
 for _ in 0..< count {
  appendLiteral(str)
  }
 }
}
print("say, \(repat: "hello!,", 3)

他のSwiftの機能と組み合わせて例えば配列がないときはデフォルト表示するなどできる。

extension String.StringInterpolation {
 mutating func appendInterpolation(_ value:[String], empty defaultValue:@autoclosure () -> String) {
  if value.count == 0
     appendLiteral(defaultValue())
  } else {
    appendLiteral(values.joined(separator:","))
  }
 }
}

let particle = ["fermion", "boson"]
print("List of particle: \(names, empty: "No one").")

@autoclosureを使うことで単純なあたいや、複雑な関数を呼び出しできる。

ExpressibleByStringLiteralやExpressibleByStringInterpolationなどと組み合わせることで
文字列補間で全体の型を作れるようになり、CustomStringConvertibleを通してその型をprintできるようにさえなった。

必要なことは
ExpressibleByStringLiteralとExpressibleByStringInterpolationに準拠することで必要ならCustomStringConvertibleにも準拠すること。
自分の型の中にネストしてStringInterpolation構造体を作りStringInterpolationProtocolに準拠させる。その初期化には期待する大まかなデータ量を入れる必要がある。また、appendLiteral() を実装する必要がある。appendInterpolation()もいる。
初期化も2ついる。

様々な共通エレメントからHTMLを構成する型を例につくろう。

struct HTMLComponent: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible { struct StringInterpolation: StringInterpolationProtocol { // start with an empty string var output = ""

    // allocate enough space to hold twice the amount of literal text
    init(literalCapacity: Int, interpolationCount: Int) {
        output.reserveCapacity(literalCapacity * 2)
    }

    // a hard-coded piece of text – just add it
    mutating func appendLiteral(_ literal: String) {
        print("Appending \(literal)")
        output.append(literal)
    }

    // a Twitter username – add it as a link
    mutating func appendInterpolation(twitter: String) {
        print("Appending \(twitter)")
        output.append("<a href=\"https://twitter/\(twitter)\">@\(twitter)</a>")
    }

    // an email address – add it using mailto
    mutating func appendInterpolation(email: String) {
        print("Appending \(email)")
        output.append("<a href=\"mailto:\(email)\">\(email)</a>")
    }
}

// the finished text for this whole component
let description: String

// create an instance from a literal string
init(stringLiteral value: String) {
    description = value
}

// create an instance from an interpolated string
init(stringInterpolation: StringInterpolation) {
    description = stringInterpolation.output
}

}




let text: HTMLComponent = "You should follow me on Twitter (twitter: "twostraws"), or you can email me at (email: "paul@hackingwithswift.com")." print(text) /// You should follow me on Twitter @twostraws, or you can email me at paul@hackingwithswift.com.

これでHTMLが吐き出されるが、他にも使い方がありそう。

参考文献

[https://github.com/twostraws/whats-new-in-swift-5-0:title]

Intrinsic Content Size

以下を呼んだメモ

iOSのAutoLayoutにおけるIntrinsic Content Sizeについて - Qiita

ビューを表示するための最低サイズ UIViewのプロパティとしてある。 読み取り専用だがオーバーライドはできる。

StoryBoard/XIBで一時的に設定できるがこの設定は実行時には影響しない。

Content Hugging Priority コンテンツサイズよりも大きくなりにくさ Compression Resistance Priority コンテンツサイズよりも小さくなりにくさ ちなみにNSLayoutConstraintは1000なのでこれらより大きい。

UIButon 250, 750 UILabel 251,750 UIImageView 251, 750 UISwitch 750, 750 UIActivityIndicatorView 750, 750

ViewのサイズがintrinsicContentSize以上だとContent Hugging Priorityの値 ViewのサイズがintrinsicContentSize以下だとContent Compression Resistance Priorityの値

pdfをepsにする

ひとまずこんな感じでやってみた。

#!/bin/bash

# echo "$1"
# 拡張子を除く
BASE_FILE_NAME=`basename "$1" .pdf`
# echo $BASE_FILE_NAME

pdf2ps "$1"
ps2eps ${BASE_FILE_NAME}.ps

bashの参考

Bashの便利な構文だがよく忘れてしまうものの備忘録 - Qiita

bashで拡張子を除いたファイル名を取得する - Qiita

Stackの中のカスタムビュー

Stack内でカスタムで作ったViewが縮む問題、以下で解決できた。

                NSLayoutConstraint.activate(
                    view.widthAnchor.constraint(equalToConstant: 320),
                    view.heightAnchor.constraint(equalToConstant: 320)
                )

スキーム設定

スキーム追加

Product->Edit Schemeでduplicate

Config追加

project file, Project, InfoタグのConfigurationで追加 スキームのエディットでrunを追加したConfigに設定

プリプロセッサ定義

prroject fileのBuild settingのPreprocessor Macroにそれぞれのコンフィグで定義したいマクロ変数を追加、MOCK=1等 また、Swiftから使えるように Other Swift Flagsに-DMOCKなどのように-D{名前}を入れる。

定義したものを使う

struct Const {
    static var apiBase: String {
        #if MOCK
            return "http://hoge.local:3000/api/v1/"
        #elseif DEBUG
            return "http://hogedebug.local:3000/api/v1/"
        #else
            return "https://hoge.com:3000/api/v2/"
        #endif
    }
    
    static var isMock: Bool {
        #if MOCK
            return true
        #else
            return false
        #endif
    }
}

フォント指定

stackoverflow.com

指定できるフォントを調べるには

        for family in UIFont.familyNames.sorted() {
            let names = UIFont.fontNames(forFamilyName: family)
            print("Family: \(family) Font names: \(names)")
        }

フォントの指定は

                Text("acccdefああ投稿").font(.custom("CourierNewPS-ItalicMT", size: 20))

のようにする。

先程のフォント名一覧はこのようにでた。

Family: Academy Engraved LET Font names: ["AcademyEngravedLetPlain"]
Family: Al Nile Font names: ["AlNile", "AlNile-Bold"]
Family: American Typewriter Font names: ["AmericanTypewriter-CondensedBold", "AmericanTypewriter-Condensed", "AmericanTypewriter-CondensedLight", "AmericanTypewriter", "AmericanTypewriter-Bold", "AmericanTypewriter-Semibold", "AmericanTypewriter-Light"]
Family: Apple Color Emoji Font names: ["AppleColorEmoji"]
Family: Apple SD Gothic Neo Font names: ["AppleSDGothicNeo-Thin", "AppleSDGothicNeo-Light", "AppleSDGothicNeo-Regular", "AppleSDGothicNeo-Bold", "AppleSDGothicNeo-SemiBold", "AppleSDGothicNeo-UltraLight", "AppleSDGothicNeo-Medium"]
Family: Apple Symbols Font names: ["AppleSymbols"]
Family: Arial Font names: ["Arial-BoldMT", "Arial-BoldItalicMT", "Arial-ItalicMT", "ArialMT"]
Family: Arial Hebrew Font names: ["ArialHebrew-Bold", "ArialHebrew-Light", "ArialHebrew"]
Family: Arial Rounded MT Bold Font names: ["ArialRoundedMTBold"]
Family: Avenir Font names: ["Avenir-Oblique", "Avenir-HeavyOblique", "Avenir-Heavy", "Avenir-BlackOblique", "Avenir-BookOblique", "Avenir-Roman", "Avenir-Medium", "Avenir-Black", "Avenir-Light", "Avenir-MediumOblique", "Avenir-Book", "Avenir-LightOblique"]
Family: Avenir Next Font names: ["AvenirNext-Medium", "AvenirNext-DemiBoldItalic", "AvenirNext-DemiBold", "AvenirNext-HeavyItalic", "AvenirNext-Regular", "AvenirNext-Italic", "AvenirNext-MediumItalic", "AvenirNext-UltraLightItalic", "AvenirNext-BoldItalic", "AvenirNext-Heavy", "AvenirNext-Bold", "AvenirNext-UltraLight"]
Family: Avenir Next Condensed Font names: ["AvenirNextCondensed-Heavy", "AvenirNextCondensed-MediumItalic", "AvenirNextCondensed-Regular", "AvenirNextCondensed-UltraLightItalic", "AvenirNextCondensed-Medium", "AvenirNextCondensed-HeavyItalic", "AvenirNextCondensed-DemiBoldItalic", "AvenirNextCondensed-Bold", "AvenirNextCondensed-DemiBold", "AvenirNextCondensed-BoldItalic", "AvenirNextCondensed-Italic", "AvenirNextCondensed-UltraLight"]
Family: Baskerville Font names: ["Baskerville-SemiBoldItalic", "Baskerville-SemiBold", "Baskerville-BoldItalic", "Baskerville", "Baskerville-Bold", "Baskerville-Italic"]
Family: Bodoni 72 Font names: ["BodoniSvtyTwoITCTT-Bold", "BodoniSvtyTwoITCTT-BookIta", "BodoniSvtyTwoITCTT-Book"]
Family: Bodoni 72 Oldstyle Font names: ["BodoniSvtyTwoOSITCTT-BookIt", "BodoniSvtyTwoOSITCTT-Book", "BodoniSvtyTwoOSITCTT-Bold"]
Family: Bodoni 72 Smallcaps Font names: ["BodoniSvtyTwoSCITCTT-Book"]
Family: Bodoni Ornaments Font names: ["BodoniOrnamentsITCTT"]
Family: Bradley Hand Font names: ["BradleyHandITCTT-Bold"]
Family: Chalkboard SE Font names: ["ChalkboardSE-Bold", "ChalkboardSE-Light", "ChalkboardSE-Regular"]
Family: Chalkduster Font names: ["Chalkduster"]
Family: Charter Font names: ["Charter-BlackItalic", "Charter-Bold", "Charter-Roman", "Charter-Black", "Charter-BoldItalic", "Charter-Italic"]
Family: Cochin Font names: ["Cochin-Italic", "Cochin-Bold", "Cochin", "Cochin-BoldItalic"]
Family: Copperplate Font names: ["Copperplate-Light", "Copperplate", "Copperplate-Bold"]
Family: Courier Font names: ["Courier-BoldOblique", "Courier-Oblique", "Courier", "Courier-Bold"]
Family: Courier New Font names: ["CourierNewPS-ItalicMT", "CourierNewPSMT", "CourierNewPS-BoldItalicMT", "CourierNewPS-BoldMT"]
Family: DIN Alternate Font names: ["DINAlternate-Bold"]
Family: DIN Condensed Font names: ["DINCondensed-Bold"]
Family: Damascus Font names: ["DamascusBold", "DamascusLight", "Damascus", "DamascusMedium", "DamascusSemiBold"]
Family: Devanagari Sangam MN Font names: ["DevanagariSangamMN", "DevanagariSangamMN-Bold"]
Family: Didot Font names: ["Didot-Bold", "Didot", "Didot-Italic"]
Family: Euphemia UCAS Font names: ["EuphemiaUCAS", "EuphemiaUCAS-Italic", "EuphemiaUCAS-Bold"]
Family: Farah Font names: ["Farah"]
Family: Futura Font names: ["Futura-CondensedExtraBold", "Futura-Medium", "Futura-Bold", "Futura-CondensedMedium", "Futura-MediumItalic"]
Family: Galvji Font names: ["Galvji-Bold", "Galvji"]
Family: Geeza Pro Font names: ["GeezaPro-Bold", "GeezaPro"]
Family: Georgia Font names: ["Georgia-BoldItalic", "Georgia-Italic", "Georgia", "Georgia-Bold"]
Family: Gill Sans Font names: ["GillSans-Italic", "GillSans-SemiBold", "GillSans-UltraBold", "GillSans-Light", "GillSans-Bold", "GillSans", "GillSans-SemiBoldItalic", "GillSans-BoldItalic", "GillSans-LightItalic"]
Family: Helvetica Font names: ["Helvetica-Oblique", "Helvetica-BoldOblique", "Helvetica", "Helvetica-Light", "Helvetica-Bold", "Helvetica-LightOblique"]
Family: Helvetica Neue Font names: ["HelveticaNeue-UltraLightItalic", "HelveticaNeue-Medium", "HelveticaNeue-MediumItalic", "HelveticaNeue-UltraLight", "HelveticaNeue-Italic", "HelveticaNeue-Light", "HelveticaNeue-ThinItalic", "HelveticaNeue-LightItalic", "HelveticaNeue-Bold", "HelveticaNeue-Thin", "HelveticaNeue-CondensedBlack", "HelveticaNeue", "HelveticaNeue-CondensedBold", "HelveticaNeue-BoldItalic"]
Family: Hiragino Maru Gothic ProN Font names: ["HiraMaruProN-W4"]
Family: Hiragino Mincho ProN Font names: ["HiraMinProN-W3", "HiraMinProN-W6"]
Family: Hiragino Sans Font names: ["HiraginoSans-W3", "HiraginoSans-W6", "HiraginoSans-W7"]
Family: Hoefler Text Font names: ["HoeflerText-Italic", "HoeflerText-Black", "HoeflerText-Regular", "HoeflerText-BlackItalic"]
Family: Kailasa Font names: ["Kailasa-Bold", "Kailasa"]
Family: Kefa Font names: ["Kefa-Regular"]
Family: Khmer Sangam MN Font names: ["KhmerSangamMN"]
Family: Kohinoor Bangla Font names: ["KohinoorBangla-Regular", "KohinoorBangla-Semibold", "KohinoorBangla-Light"]
Family: Kohinoor Devanagari Font names: ["KohinoorDevanagari-Regular", "KohinoorDevanagari-Light", "KohinoorDevanagari-Semibold"]
Family: Kohinoor Gujarati Font names: ["KohinoorGujarati-Light", "KohinoorGujarati-Bold", "KohinoorGujarati-Regular"]
Family: Kohinoor Telugu Font names: ["KohinoorTelugu-Regular", "KohinoorTelugu-Medium", "KohinoorTelugu-Light"]
Family: Lao Sangam MN Font names: ["LaoSangamMN"]
Family: Malayalam Sangam MN Font names: ["MalayalamSangamMN-Bold", "MalayalamSangamMN"]
Family: Marker Felt Font names: ["MarkerFelt-Thin", "MarkerFelt-Wide"]
Family: Menlo Font names: ["Menlo-BoldItalic", "Menlo-Bold", "Menlo-Italic", "Menlo-Regular"]
Family: Mishafi Font names: ["DiwanMishafi"]
Family: Mukta Mahee Font names: ["MuktaMahee-Light", "MuktaMahee-Bold", "MuktaMahee-Regular"]
Family: Myanmar Sangam MN Font names: ["MyanmarSangamMN", "MyanmarSangamMN-Bold"]
Family: Noteworthy Font names: ["Noteworthy-Bold", "Noteworthy-Light"]
Family: Noto Nastaliq Urdu Font names: ["NotoNastaliqUrdu", "NotoNastaliqUrdu-Bold"]
Family: Noto Sans Kannada Font names: ["NotoSansKannada-Bold", "NotoSansKannada-Light", "NotoSansKannada-Regular"]
Family: Noto Sans Myanmar Font names: ["NotoSansMyanmar-Regular", "NotoSansMyanmar-Bold", "NotoSansMyanmar-Light"]
Family: Noto Sans Oriya Font names: ["NotoSansOriya-Bold", "NotoSansOriya"]
Family: Optima Font names: ["Optima-ExtraBlack", "Optima-BoldItalic", "Optima-Italic", "Optima-Regular", "Optima-Bold"]
Family: Palatino Font names: ["Palatino-Italic", "Palatino-Roman", "Palatino-BoldItalic", "Palatino-Bold"]
Family: Papyrus Font names: ["Papyrus-Condensed", "Papyrus"]
Family: Party LET Font names: ["PartyLetPlain"]
Family: PingFang HK Font names: ["PingFangHK-Medium", "PingFangHK-Thin", "PingFangHK-Regular", "PingFangHK-Ultralight", "PingFangHK-Semibold", "PingFangHK-Light"]
Family: PingFang SC Font names: ["PingFangSC-Medium", "PingFangSC-Semibold", "PingFangSC-Light", "PingFangSC-Ultralight", "PingFangSC-Regular", "PingFangSC-Thin"]
Family: PingFang TC Font names: ["PingFangTC-Regular", "PingFangTC-Thin", "PingFangTC-Medium", "PingFangTC-Semibold", "PingFangTC-Light", "PingFangTC-Ultralight"]
Family: Rockwell Font names: ["Rockwell-Italic", "Rockwell-Regular", "Rockwell-Bold", "Rockwell-BoldItalic"]
Family: Savoye LET Font names: ["SavoyeLetPlain"]
Family: Sinhala Sangam MN Font names: ["SinhalaSangamMN-Bold", "SinhalaSangamMN"]
Family: Snell Roundhand Font names: ["SnellRoundhand", "SnellRoundhand-Bold", "SnellRoundhand-Black"]
Family: Symbol Font names: ["Symbol"]
Family: Tamil Sangam MN Font names: ["TamilSangamMN", "TamilSangamMN-Bold"]
Family: Thonburi Font names: ["Thonburi", "Thonburi-Light", "Thonburi-Bold"]
Family: Times New Roman Font names: ["TimesNewRomanPS-ItalicMT", "TimesNewRomanPS-BoldItalicMT", "TimesNewRomanPS-BoldMT", "TimesNewRomanPSMT"]
Family: Trebuchet MS Font names: ["TrebuchetMS-Bold", "TrebuchetMS-Italic", "Trebuchet-BoldItalic", "TrebuchetMS"]
Family: Verdana Font names: ["Verdana-Italic", "Verdana", "Verdana-Bold", "Verdana-BoldItalic"]
Family: Zapf Dingbats Font names: ["ZapfDingbatsITC"]
Family: Zapfino Font names: ["Zapfino"]

UITextViewをSwiftUIで用いる。

makeUIView, updateUIView, makeCoordinatorを実装する。Coordinatorはdelegateを処理する。

import UIKit
import Combine
// https://www.appcoda.com/swiftui-textview-uiviewrepresentable/
struct EditView: UIViewRepresentable {
    
    @Binding var text: String
    @Binding var textStyle:UIFont.TextStyle
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        
        textView.font = UIFont.preferredFont(forTextStyle: textStyle)
        textView.autocapitalizationType = .sentences
        textView.isSelectable = true
        textView.isUserInteractionEnabled = true
        textView.delegate = context.coordinator
        
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        uiView.text = text
        uiView.font = UIFont.preferredFont(forTextStyle: textStyle)
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator($text)
    }
}

class Coordinator: NSObject, UITextViewDelegate {
    var text: Binding<String>
    
    init(_ text:Binding<String>) {
        self.text = text
    }
    
    func textViewDidChange(_ textView: UITextView) {
        self.text.wrappedValue = textView.text
    }
    
    func textViewDidBeginEditing(_ textView: UITextView) {
    }
}

使う側は単に呼び出す

struct ContentView: View {
    
    @State private var message = "x"
    @State private var textStyle = UIFont.TextStyle.body
    
    var body: some View {
        VStack(alignment: .leading) {
            Button(action: {
                UIApplication.shared.endEditing()
            }, label: {
                Text("Push")
            })
            Text(message)
            EditView(text: $message, textStyle: $textStyle)
        }
    }
}

今回、キーボードを下げるのは

import UIKit

extension UIApplication {
    func endEditing() {
        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}

というextensionを用いた。

簡単な画面遷移

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

簡単なViewModel

ごく簡単にViewModelを作ってみた。

まずモデルは

struct Article: Codable {
    let title:String
    let url:String
}

func fetchArticles() -> AnyPublisher<[Article], Error> {
    let url = URL(string: "https://qiita.com/api/v2/items")!
    let request = URLRequest(url:url)
    
    return URLSession.shared
        .dataTaskPublisher(for: request)
        .map({$0.data})
        .decode(type: [Article].self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

ViewModelはこのようにボタンタップをしたらtextが変わる形に書いておく。 ObservableObjectを継承する。

import Foundation
import SwiftUI
import Combine

class ViewModel : ObservableObject {
    @Published private(set) var text: String = "Hello, World!"
    var cancels = [AnyCancellable]()
    func onTapped() {
        fetchArticles()
         .receive(on: DispatchQueue.main)
         .sink(receiveCompletion: {_ in
         }) { articles in
            
             self.text = articles.description
         }.store(in: &cancels)
    }
}

ViewでViewModelをつなげる。@ObservedObjectを使う。

import SwiftUI
import Combine


struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()
    var body: some View {
        VStack {
        Button(action: {
            self.viewModel.onTapped()
        }) {
            Text("Push")
        }
            Text(viewModel.text)
        }
    }
}

ここではエラーについて議論していない。