20210606のiOSに関する記事は4件です。

iOS14時代のSVG表示設定方法

デザイナーからFigmaのURLが1つ送られてきて「後は勝手にやれ」と言われる機会が増えてきたこの頃。 iOS14時代のアプリでSVGを扱う方法は何が最適か試行錯誤してみました。 (Xcode 12.5, Deployment Target = iOS 12.0 と 13.0) Devployment Target = 13.0 以降 ImageSetにSVGを直接登録して構いません。 Asset CatalogにImage Setを作成する。 Resizing Preserve Vector Dataにチェックする。 Scalesは Single Scaleにセットする。 SVGをAllにセットする。 (Dark Mode対応の場合) Devployment Target = 12.0 残念ながらSVGをPDFに変換する必要があります。 そうしないと、画像がギザギザになってしまいます。 Figmaだと画像をPDFで保存できるので少し楽になりましたね。 Asset CatalogにImage Setを作成する。 Resizing Preserve Vector Dataにチェックする。 Scalesは Single Scaleにセットする。 PDFをAllにセットする。 SVG PDF それでは、”iOS15時代の〜”でお目にかかりましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SQLite.swift でSQLiteを操作する

まえがき Swiftに入門中の友人に向けて説明している記事になります。 SQLite.swift 導入手順 今回はSwift Package Managerで導入する Xcodeから File > Swift Package > Add Package Dependency を選択 https://github.com/stephencelis/SQLite.swift.git で検索 Versionを選択(今回は特に何も考えずデフォルト設定) Finish! XcodeのUI上で入れたPackageも確認できる! SQLiteを操作してみる 今回やることは以下 - データベースファイルを作成 - userテーブルを作成 - userの追加(Insert) - userの検索(Select) 1. データベースファイルを作成 Databaseクラス作成を作成して下記を実装 (ViewControllerクラスで実装してもいいけど分けておいたほうがわかりやすそうなので今回はクラスを分ける) Database.swift import Foundation import SQLite let FILE_NAME = "sample.db" class Database { var db: Connection init() { // DBファイルの作成先のパスを生成 let filePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(FILE_NAME).path // DBファイル作成/開く db = try! Connection(filePath) } } ちゃんとViewController側で初期化することを忘れずに。 ViewController.swift import UIKit class ViewController: UIViewController { var database = Database() override func viewDidLoad() { super.viewDidLoad() } } 実行後Finderでディレクトリを見てみると無事ファイルが生成されています。 2.userテーブルを作成 Database.swift class Datastore { var db: Connection init() { // DBファイルの作成先のパスを生成 let filePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(FILE_NAME).path // DBファイル作成/開く db = try! Connection(filePath) // DBにテーブル作成 let users = Table("users") do { try db.run(users.create { t in t.column(Expression<Int64>("id"), primaryKey: true) t.column(Expression<String?>("name")) t.column(Expression<String>("email"), unique: true) }) } catch {} } } do {} catch {} でくくってる理由は2回目移行で動かす時にすでにテーブルが作成されている状態になるのでもう一度作ろうとしてエラーになるのでそれを無視するため。(もっといい書き方ありそうなきもするけど一旦これでいいかな) SQLiteの中を見れるGUIで確認するとちゃんと作成できてることが確認できる。 ごちゃごちゃしてきたのでUserDatastoreってクラス作ってUserテーブルはそっちで触るような実装に整理することにした。 Database.swift class Database { let db: Connection let userDatastore: UserDatastore init() { // DBファイルの作成先のパスを生成 let filePath = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(FILE_NAME).path // DBファイル作成/開く db = try! Connection(filePath) // UserDatastoreを初期化 userDatastore = UserDatastore(db: db) } } class UserDatastore { private let table = Table("users") private let id = Expression<Int64>("id") private let name = Expression<String>("name") private let email = Expression<String>("email") private let db: Connection init(db: Connection) { self.db = db do { try self.db.run(table.create { t in t.column(Expression<Int64>("id"), primaryKey: true) t.column(Expression<String>("name")) t.column(Expression<String>("email"), unique: true) }) } catch {} } } 3. userの追加(Insert) 初期データでも入れてみる。 userテーブルの初期データというていで実装する(マイグレーション) Database.swift class Database { 省略... } class UserDatastore { private let table = Table("users") private let id = Expression<Int64>("id") private let name = Expression<String>("name") private let email = Expression<String>("email") private let db: Connection init(db: Connection) { self.db = db do { try self.db.run(table.create { t in t.column(Expression<Int64>("id"), primaryKey: true) t.column(Expression<String>("name")) t.column(Expression<String>("email"), unique: true) }) // 初期データを入れる let migrationItems = [ ["name": "Alice", "email":"alice@mac.com"], ["name": "Bob", "email":"bob@mac.com"] ] migrationItems.forEach { row in try? insert(name: row["name"]!, email: row["email"]!) } } catch {} } func insert(name: String, email: String) throws { let insert = table.insert(self.name <- name, self.email <- email) try db.run(insert) } } 内容的にはinsertの関数を作成、データでぐるぐるループしてinsertの関数を実行する。 try self.db.run(table.create... の実行で2回目移行はエラーが発生して後続の処理(ループしてインサートする処理)が動かなくなるので初回の1回目しか実行されないため初期データが2重で入ることもない。 (ちゃんとマイグレーションするなら作り込んだほうがいいかもだけど一旦これで。) 中みるとちゃんとデータが入ってる。(2回実行されてもデータが増えない) データが入らない場合はsample.dbのファイルごと一回消してやってみる。 4.userの検索(Select) Userテーブルに入ってるすべてのデータを取得してTableViewで表示してみる Database.swift class UserDatastore { 省略... func find() -> [User] { var results = [User]() // エラー起こした場合は空の配列を返却 guard let users = try? db.prepare(table) else { return results } for row in users { results.append(User(id: row[self.id], name: row[self.name], email: row[self.email])) } return results } } class User { let id: Int64 let name: String let email: String init(id: Int64, name: String, email: String) { self.id = id self.name = name self.email = email } } Userクラスを追加したのとUserDatastoreクラスにfindの関数を実装 ViewController.swift class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { var database = Database() var datas = [User]() override func viewDidLoad() { super.viewDidLoad() datas = database.userDatastore.find() } // Sectionの個数 func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return datas.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let identifier = "HogeCell" let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) cell.textLabel?.text = datas[indexPath.row].name return cell } } ユーザIDで検索もしたい。 Database.swift class UserDatastore { 省略... func findById(id: Int64) -> [User] { var results = [User]() // エラー起こした場合は空の配列を返却 guard let users = try? db.prepare(table.where(self.id == id)) else { return results } for row in users { results.append(User(id: row[self.id], name: row[self.name], email: row[self.email])) } return results } } ViewController.swift class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { var database = Database() var datas = [User]() override func viewDidLoad() { super.viewDidLoad() // datas = database.userDatastore.find() datas = database.userDatastore.findById(id: 2) } 省略... 感想 初めてちゃんとQita書いた気がするけどなんかちょっとモチベーション上がったから定期的にかけるといいな。 Swift Package Manager 初めて使ってみたがわかりやすくて好き。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】TabViewに立体感を出してみる

はじめに 今回は、SwiftUIのTabViewに少し手を加えてちょっとおしゃれに移動するようにしたいと思います。 1.作ったもの こちらです。画面遷移の際にViewがズームアウト→移動→ズームインで元に戻る。 といった動作をします。 ※gifなのでカクカクしてますが、本来はスムーズに動作します。 このようにすることで移動動作に立体感がでて、背景色を統一することでシームレスに移動している感じが出ます。 2.ベースの実装 まずはベースとなるPageViewを作る ベースは.tabViewStyle(PageTabViewStyle())で指定している通りPageViewとなります。 今回はサンプルとして四角形3つをPageViewで切り替えるViewとして定義しています。 現在のViewを示すtag番号をselectionで定義しており、 BACKボタン、NEXTボタンで更新して遷移するようにしています。 ContentView.swift struct ContentView: View { @State private var selection : Int = 0 var body: some View { VStack { TabView(selection: self.$selection) { Rectangle() .fill(Color(.white)) .border(Color(.black)) .tag(0) .scaleEffect(self.ratio) Rectangle() .fill(Color(.red)) .border(Color(.black)) .tag(1) .scaleEffect(self.ratio) Rectangle() .fill(Color(.blue)) .border(Color(.black)) .tag(2) .scaleEffect(self.ratio) } .tabViewStyle(PageTabViewStyle()) HStack { Button("BACK") { if self.selection > 0 { self.selection -= 1 } } Button("GO") { if self.selection < 3 { self.selection += 1 } } } } } } 動作を見てみる ご覧の通りベースはできました。ここからズームインアウトのアニメーションをつけていきます。 ※解説用にデザインを簡略化して載せています。 3.ズームインアウト動作を実装する やりかた さて、本題ですがViewの大きさを.scaleEffect()の値を操作することでズームインアウトのアニメーションを実現しています。 今回の流れとしては  GOボタンを押す  →scaleEffect(0.5)にしてViewを小さくする。  →selectionを更新してViewを移動  →secaleEffect(1.0)にして倍率を戻す。 となります。 実装 Viewの倍率を状態変数で定義する。 Viewの倍率を状態変数ratioとして定義します。 そして各Rectangle()のモディファイアに.scaleEffect(self.ratio)を設定します。 こうすることでself.ratioを変えることでスケールを自由に変えることができます。 また、.transition(.slide)と.animation(.easeInOut)を設定することで 遷移するときのアニメーションを設定しています。 struct ContentView: View { @State private var ratio : CGFloat = 1.0 var body: some View { VStack { TabView(selection: self.$selection) { Rectangle() .scaleEffect(self.ratio) Rectangle() .scaleEffect(self.ratio) Rectangle() .scaleEffect(self.ratio) } .tabViewStyle(PageTabViewStyle()) .transition(.slide) .animation(.easeInOut) } } ボタンのアクションに組み込む 先ほど定義してself.ratioをボタンを押した時に0.5、 1秒後にselectionを進めてその1秒後にself.ratioを1.0に戻しています。 ”1秒後に”の部分の実装はTimerを使っています。 (Timerについてはこちらに記載していますので興味のある方はご覧ください。) Button("BACK") { if self.selection > 0 { self.ratio = 0.5 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) {timer in self.selection -= 1 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in self.ratio = 1.0 } } } } Button("GO") { if self.selection < 3 { self.ratio = 0.5 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) {timer in self.selection += 1 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in self.ratio = 1.0 } } } } 動作を見てみる いい感じに完成しました! あとはRectangleの部分を自分の好きなViewにすることでカスタマイズができるかと思います。 おわりに 作ってみて思いましたが、Viewの遷移に毎回2秒かかるのは結構ストレスなので、 メイン画面のメニューとしては使えないかなと。 アプリのチュートリアル画面とかで順番に見せるとかであれば使えそうです。 また、一番最初のデモで見せたように枠線をなくして背景色を全部同じにすると、 シームレスに移動しているように見えます。 そしてスケールする対象を変えるとこのようにボタンだけ正面に残しておく といったこともできますので自由にカスタマイズしてみてください。 おまけ 今回使用したコードの全文をこちらです。 ContentView.swift import SwiftUI struct ContentView: View { @State private var selection : Int = 0 @State private var ratio : CGFloat = 1.0 var body: some View { VStack { TabView(selection: self.$selection) { Rectangle() .fill(Color(.white)) .border(Color(.black)) .tag(0) .scaleEffect(self.ratio) Rectangle() .fill(Color(.red)) .border(Color(.black)) .tag(1) .scaleEffect(self.ratio) Rectangle() .fill(Color(.blue)) .border(Color(.black)) .tag(2) .scaleEffect(self.ratio) } .tabViewStyle(PageTabViewStyle()) .transition(.slide) .animation(.easeInOut) HStack { Button("BACK") { if self.selection > 0 { self.ratio = 0.5 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) {timer in self.selection -= 1 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in self.ratio = 1.0 } } } } Button("GO") { if self.selection < 3 { self.ratio = 0.5 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) {timer in self.selection += 1 Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in self.ratio = 1.0 } } } } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIでswipe→progress view表示→データ更新→更新完了次第 progress view非表示を実現する

環境 Xcode…Version 12.5 (12E262) やりたかったことと経緯 ScrollViewにデータを表示しておいて、スワイプされたらデータ更新。 progress view(くるくる)を表示する。 新しいデータを取得完了次第progress viewを表示終了する。 というのを実現したかったのだが、そもそもSwiftUIはprogress view単体はあるもの(ProgessViewというstruct)の、「データ更新終了次第消す」機能どころか「スワイプされたらくるくるを出す」機能(androidでいうSwipeRefreshLayout、 UIKitのiOSでいうUIRefreshControl?)すらまだデフォルトでの用意はないらしい。なので自作した。 コード 1.progress viewを表示する側の準備をする① struct TopView: View { @StateObject var apiModel :ApiModel var body: some View { VStack{ if(self.apiModel.memos.count>0){ MemoList(memos: self.apiModel.memos).environmentObject(apiModel) }else{ EmptyView() } } .coordinateSpace(name: "parent") .onAppear(){ // 表示したら読み込み開始 self.apiModel.load() } } } class ApiModel: ObservableObject { @Published var memos : Array<String> = [] init() { } private let API_BASE = "https://hogehoge" private let API_PATH_GET_ALL_DATA="/apiNameGetAll" public func load(memosContainer : Binding<Array<String>>? = nil) { let url = URL(string: API_BASE+API_PATH_GET_ALL_DATA)! URLSession.shared.dataTask(with: url) { data, response, error in print("data: \(String(describing: data))") do{ let allData = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) print(allData) // Jsonの中身を表示 } catch { print(error) } DispatchQueue.main.async { if(memosContainer == nil){ self.memos = try! JSONDecoder().decode(Array<String>.self, from: data!) }else{ memosContainer!.wrappedValue = try! JSONDecoder().decode(Array<String>.self, from: data!) } } }.resume() } } TopView : 大元のView。MemoListはScrollViewを持ち、中でデータをViewにして表示している。(具体的コードは後述。) ApiModel : http通信をして外からデータを取得してくるメソッドload(memosContainer : Binding<Array<String>>? = nil)と取得してきたデータの入っている@Published var memos : Array<String>を持つ。 load(memosContainer : Binding<Array<String>>? = nil)は、memosContainerが渡されなかったら取得してきたデータを@Published var memos : Array<String>に入れるが、not-nilだったら、memosContainerにデータを入れる。 以上のsnipetでのポイントは.coordinateSpace(name: "parent")。これがあとでprogress viewを出す時にきいてくる。 2.progress viewを表示するための準備②+データ更新終了時にprogress viewの表示を消す部分の実装 struct MemoList:View { var memos : Array<String> @EnvironmentObject var apiModel :ApiModel init(memos : Array<String>) { self.memos = memos } var body: some View{ var swipe : SwipeRefreshController? // 中間橋渡しをしてくれるオブジェクトを作ることで、データが更新されたらクルクルを消せるようにする。 let p = Binding<Array<String>>(get : { return self.apiModel.memos },set : { memos in swipe?.finishRefresh() // データ取得が終わったのでクルクルを止める self.apiModel.memos = memos }) swipe = SwipeRefreshController(coordinateSpaceName:"parent"){ self.apiModel.load(memosContainer: p) } return ScrollView(.vertical){ VStack{ swipe ForEach(self.memos, id: \.self){ memo in Text(memo) } } } } } MemoList : ScrollViewにデータを表示していくView。 また、今回の主役のprogress viewをもつViewであるSwipeRefreshControllerもScrollViewの中に入れて表示する。 SwipeRefreshControllerはinit時に(coordinateSpaceName : String,onRefresh : @escaping (()->Void)を引数に取る。これは引っ張られたかどうか判断するために親のcoordinateSpaceのnameを渡すためのcoordinateSpaceName : Stringと、progress viewを表示した時の表示開始(正確にはスワイプされた時)コールバックのonRefresh : @escaping (()->Void)。(コード詳細後述) また、SwipeRefreshControllerはfinishRefresh()を呼ぶとprogress viewを表示終了する。(コード詳細後述) 以上のsnipetでのポイントは、まず、「progress viewに表示するための準備①」でTopViewに設定したcoordinateSpace nameの"parent"をSwipeRefreshControllerインスタンス作成時に渡していること。 更に let p = Binding<Array<String>>(get : { return self.apiModel.memos },set : { memos in swipe?.finishRefresh() // データ取得が終わったのでクルクルを止める self.apiModel.memos = memos }) を作成し、SwipeRefreshControllerのprogress view表示開始コールバックにself.apiModel.load(memosContainer: p)を設定したこと。これにより、スワイプされてprogress viewが表示されたら、データの更新ApiModel#load(memosContainer : Binding<Array<String>>? = nil)が呼ばれ、さらに今回はmemosContainer!=nilなのでデータ更新され次第pにとってきたデータが入り、p#setが呼ばれる。 そしてp#setの中でSwipeRefreshController#finishRefresh()を呼ぶことで、「データの更新が終了し次第progress viewの表示を終了」を実現。さらにself.apiModel.memos = memosを呼ぶことでTopViewやMemoListが表示に使用しているおおもとのデータも更新する。 3.スワイプされたら感知してprogress viewを表示するViewの作成 class RefreshManager : ObservableObject { @Published var isRefreshing = false } // スワイプしたら更新中のクルクルをだしてリフレッシュコールバックを呼ぶためのView struct SwipeRefreshController: View { init(coordinateSpaceName : String,onRefresh : @escaping (()->Void)) { // クロージャがスコープ外でも生きる時は@escapeが必要 https://qiita.com/mishimay/items/1232dbfe8208e77ed10e#%E3%81%A9%E3%81%86%E3%81%84%E3%81%86%E3%81%A8%E3%81%8D%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%8B self.coordinateSpaceName = coordinateSpaceName self.onRefresh = onRefresh //isRefreshing = false } @ObservedObject var refreshManager = RefreshManager() // @Stateでただのbool値にしていると、外からfalseにしても反映されなかったので、ObservaleObjectクラスを作ってその中のプロパティにする。 // クルクルを消す public func finishRefresh(){ print("finishRefresh") self.refreshManager.isRefreshing = false } var coordinateSpaceName: String var onRefresh: () -> Void // スワイプをした時のコールバック var body: some View { return GeometryReader { geometry in VStack{ if geometry.frame(in: .named(coordinateSpaceName)).midY > 100 { Spacer() .onAppear() { //isRefreshing = true self.refreshManager.isRefreshing = true onRefresh() } } if (self.refreshManager.isRefreshing) { HStack { Spacer() ProgressView() Spacer() } } } } } } 渡されたcoordinateSpaceNameと、GeometryReaderでスワイプされたかどうか判定して、スワイプされたらclass RefreshManager : ObservableObjectに持たせたデータ更新中フラグの@Published var isRefreshingをたて、init時に渡されていたスワイプ時コールバックを呼ぶ。 また、isRefreshingフラグが立ったらprogress viewを表示し、おろされたら表示を終了する。 以上で、 ScrollViewにデータを表示しておいて、スワイプされたらデータ更新。 progress view(くるくる)を表示する。 新しいデータを取得完了次第progress viewを表示終了する。 を実現できました。 参考にしたもの https://www.yururiwork.net/%E3%80%90swiftui%E3%80%91pull-to-refresh%EF%BC%88uirefreshcontrol%EF%BC%89%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B/ https://qiita.com/masa7351/items/0567969f93cc88d714ac
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む