空白をチェックしているようだ。
javascriptで poge = {oiu:“”, hoge:“a”} !!poge.oiu -> false !!hoge -> true
空白をチェックしているようだ。
javascriptで poge = {oiu:“”, hoge:“a”} !!poge.oiu -> false !!hoge -> true
ひとまずこんな感じでやってみた。
#!/bin/bash # echo "$1" # 拡張子を除く BASE_FILE_NAME=`basename "$1" .pdf` # echo $BASE_FILE_NAME pdf2ps "$1" ps2eps ${BASE_FILE_NAME}.ps
dynamicLinkなどもこれで確かめられる。
xcrun simctl openurl booted https//hogehoge.page.link/***
Stack内でカスタムで作ったViewが縮む問題、以下で解決できた。
NSLayoutConstraint.activate( view.widthAnchor.constraint(equalToConstant: 320), view.heightAnchor.constraint(equalToConstant: 320) )
Product->Edit Schemeでduplicate
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 } }
routerにsessionをuseしていた。 appに対してsessionをuseする必要があった。
sudo chown -R $(whoami) $(brew --prefix)/*
指定できるフォントを調べるには
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"]
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を作ってみた。
まずモデルは
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) } } }
ここではエラーについて議論していない。
PassthroughSubject と CurrentValueSubject
を使ってみた。おおよそRxSwift的に使えると思った。
func test() -> Void { let pub = PassthroughSubject<Int, Error>() _ = pub .sink(receiveCompletion: { _ in }) { value in print("new val -> \(value)") } pub.send(10) pub.send(100) pub.send(completion: .finished) } //test() func test2() -> Void { let pub = CurrentValueSubject<Int, Error>(100) _ = pub.sink(receiveCompletion: {_ in }, receiveValue: { (value) in print("new val -> \(value)") }) pub.send(100) pub.send(1000) pub.send(completion: .finished) } //test2() func test3() -> Void { let pub = [1,2,3,4,5,6,7,8,9,10].publisher _ = pub.sink(receiveValue: { value in print("new val -> \(value)") }) } //test3() func test4() -> Void { let pub = [1,2,3,4,5].publisher let pub2 = pub.map { (x) -> Int in return x*2 } _ = pub2.sink(receiveValue: { value in print("new val -> \(value)") }) } //test4() /* new val -> (4, 5) new val -> (4, 6) new val -> (4, 7) new val -> (4, 8) */ func test5() -> Void { let pub = [1,2,3,4].publisher let pub2 = [5,6,7,8].publisher let pub3 = pub.combineLatest(pub2) _ = pub3.sink(receiveValue: { value in print("new val -> \(value)") }) } //test5() /* new val -> (1, 5) new val -> (2, 6) new val -> (3, 7) new val -> (4, 8) */ func test6() -> Void { let pub = [1,2,3,4].publisher let pub2 = [5,6,7,8].publisher let pub3 = pub.zip(pub2) _ = pub3.sink(receiveValue: { value in print("new val -> \(value)") }) } //test6() func test7() -> Void { let sub = PassthroughSubject<Int, Error>() _ = sub.sink(receiveCompletion: { comp in print("\(comp)") }, receiveValue: { (value) in print("new val -> \(value)") }) sub.send(100) sub.send(120) sub.send(completion: .failure(NSError(domain: "Error!", code: 0, userInfo: nil))) } test7()
import sys # 該当言語 searchWord = "hoge" # ファイル読み込み text = open(sys.argv[1], "r") # 一行づつ読み出し for line in text: # 該当単語があるなら書き出し if searchWord in line: print(line) # 閉じる text.close()
細かいことは無視してひとまず動かしてみる例を勉強してみた。
JSONを読み込む用のモデル
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() }
無理やり表示させてみる。
import SwiftUI import Combine var cancels = [AnyCancellable]() struct ContentView: View { @State var tex = "x" var body: some View { VStack { Button(action: { fetchArticles() .sink(receiveCompletion: {_ in }) { articles in self.tex = articles.description }.store(in: &cancels) }) { Text("a") } Text(tex) } } }
これでひとまず動かせた。 AnyPublisherがRxSwiftのObject的なもの sinkで購読、storeでキャンセル待ちを保存しておく。 実際の実装ではViewでfetch等はしないだろう。
チュートリアルより。 UIPageViewControllerをSwiftUIで使う。
まず、使いたいUIViewControllerをwrapするクラスを作る。 そのクラスはUIViewControllerRepresentableを実装する。
import Foundation import SwiftUI import UIKit struct PageViewController: UIViewControllerRepresentable { typealias UIViewControllerType = UIPageViewController var controllers:[UIViewController] // ページ情報 @Binding var currentPage:Int // 関連するdataSorce, delegate等のクラスはCoordinaterとして渡す。 func makeCoordinator() -> Coordinater { Coordinater(self) } // ここでViewControllerを作って渡す。delegate等もcontextを通じてつなげる。 func makeUIViewController(context contetxt:Context) -> UIPageViewController { let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: . horizontal) pageViewController.dataSource = contetxt.coordinator pageViewController.delegate = contetxt.coordinator return pageViewController } // 更新 func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) { pageViewController.setViewControllers([controllers[currentPage]], direction: .forward, animated: true) } // DataSorce, Delegate等はCoordinaterとして宣言する。 class Coordinater: NSObject, UIPageViewControllerDataSource,UIPageViewControllerDelegate { var parent:PageViewController init(_ pageViewController:PageViewController) { self.parent = pageViewController } func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index == 0 { return parent.controllers.last; } return parent.controllers[index - 1] } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let index = parent.controllers.firstIndex(of: viewController) else { return nil } if index + 1 == parent.controllers.count { return parent.controllers.first } return parent.controllers[index + 1] } func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed, let visibleViewController = pageViewController.viewControllers?.first, let inedx = parent.controllers.firstIndex(of:visibleViewController) { parent.currentPage = inedx } } } }
UIViewも似た感じで扱える。UIViewRepresentableを実装する。
import SwiftUI import UIKit struct PageControl: UIViewRepresentable { var numberOfPages:Int @Binding var currentPage:Int func makeUIView(context:Context) -> UIPageControl { let control = UIPageControl() control.numberOfPages = numberOfPages // controlのtarget, actionをつないでいる。 control.addTarget(context.coordinator, action: #selector(Coordinator.updateCurrentPage(sender:)), for: .valueChanged) return control } // 表示ロジック用のクラスを作る。 func makeCoordinator() -> Coordinator { Coordinator(self) } // 更新 func updateUIView(_ uiView: UIPageControl, context: UIViewRepresentableContext<PageControl>) { uiView.currentPage = currentPage } class Coordinator: NSObject { var control: PageControl init(_ control:PageControl) { self.control = control } @objc func updateCurrentPage(sender:UIPageControl) { control.currentPage = sender.currentPage } } }
これらのプロトコルを実装すれば普通のビューのように扱える。
import SwiftUI struct PageView<Page:View>: View { /// UIPageViewControllerではUIViewController達として扱うのでSwiftUIのViewをUIHostViewControllerでくるんで扱う。 var viewControllers:[UIHostingController<Page>] @State var currentPage = 0 init(_ views:[Page]) { self.viewControllers = views.map { UIHostingController(rootView: $0) } } var body: some View { ZStack(alignment: .bottomTrailing){ PageViewController(controllers: viewControllers, currentPage: $currentPage) PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage) .padding(.trailing) } } } struct PageView_Previews: PreviewProvider { static var previews: some View { PageView(features.map { FeatureCard(landmark: $0)}) .aspectRatio(3/2, contentMode: .fit) } }