ラベル Swift の投稿を表示しています。 すべての投稿を表示
ラベル Swift の投稿を表示しています。 すべての投稿を表示

2025年6月7日土曜日

Swift でリントとフォーマットをしてみる

Swift でリントとフォーマットをしてみる

概要

コード規約を作成します

環境

  • macOS 15.5
  • SwiftFormat 0.56.1
  • SwiftLint 0.59.1

インストール

  • vim Podfile
pod 'SwiftLint'
pod 'SwiftFormat/CLI'
  • pod install

フォーマット

  • ./Pods/SwiftFormat/CommandLineTool/swiftformat .

リント

  • ./Pods/SwiftLint/swiftlint .

おすすめ .swiftlint.yml 設定

excluded:
- Pods
disabled_rules:
- force_cast
- force_try
- line_length
- function_body_length

要約すると

  • force try (try!) は使っても OK
  • 1行の長さ120以上 OK
  • 1クラスの行数250以上 OK
  • 1メソッドの行数100以上 OK

です
これ以外はちゃんと対応したほうがいいと思います

その他

  • コード内で個別にリントを 無視する
    • 例えば関数の複雑性を無視する場合は関数の上に以下を記載します
// swiftlint:disable:next cyclomatic_complexity
  • コード内で個別にフォーマットさせないようにする
    • 例えば強制改行を SwiftFormat でさせないようにするには以下のように記載します
// swiftformat:disable wrapMultilineStatementBraces

最後に

フォーマットしても普通にリントで引っかかるのであとは手動で直すしかありません

参考サイト

2024年10月24日木曜日

(サンプルコード) SwiftUI で RSS フィードリーダ

(サンプルコード) SwiftUI で RSS フィードリーダ

概要

ポイントは

  • Swift 純正の XMLParser と XMLParserDelegate を使い RSS 情報をパースする
  • XMLParser で CDATA があるときは専用のメソッドを使う
  • CDATA 内にある html をパースして更に必要な情報が取得できる
  • XML の必要な情報はモデルにして SwiftUI 上でバインドする

環境

  • macOS 15.0.1
  • Xcode 16.0

RSSFeedParser.swift

RSS フィードをパースする処理を管理するクラスです

import Foundation
import Combine

class RSSFeedParser: NSObject, ObservableObject, XMLParserDelegate {
    @Published var items: [RSSItem] = []
    
    // 各種要素を管理する変数
    private var currentElement = ""
    private var currentTitle = ""
    private var currentLink = ""
    private var currentDescription = ""
    private var currentContent = ""
    private var parsingItem = false
    
    // RSS フィールドをロードしパースを開始する
    func loadRSSFeed(url: URL) {
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            guard let data = data else { return }
            
            let parser = XMLParser(data: data)
            parser.delegate = self
            parser.parse()
        }
        task.resume()
    }
    
    // XMLParser の Delegate メソッド、要素が見つかったら最初にコールされる
    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
        currentElement = elementName
        if elementName == "item" {
            currentTitle = ""
            currentLink = ""
            currentDescription = ""
            currentContent = ""
            parsingItem = true
        }
    }
    
    // XMLParser の Delegate メソッド、要素内の一般タグでも見つかったらコールされる
    func parser(_ parser: XMLParser, foundCharacters string: String) {
        if parsingItem {
            if currentElement == "title" {
                currentTitle += string
            } else if currentElement == "link" {
                currentLink += string
            } else if currentElement == "description" {
                currentDescription += string
            }
        }
    }
    
    // XMLParser の Delegate メソッド、要素内の CDATA タグでも見つかったらコールされる
    func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) {
        if parsingItem && currentElement == "content:encoded" {
            if let cdataString = String(data: CDATABlock, encoding: .utf8) {
                currentContent += cdataString
            }
        }
    }
    
    // XMLParser の Delegate メソッド、要素が終了したらコールされる
    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        if elementName == "item" {
            let imageSrc = extractImageSrc(from: currentContent)
            let item = RSSItem(title: currentTitle, link: currentLink, description: currentDescription, content: currentContent, imageSrc: imageSrc)
            DispatchQueue.main.async {
                self.items.append(item)
            }
            parsingItem = false
        }
    }
  
    // CDATA から特定の html 属性情報を抽出するヘルパーメソッド
    func extractImageSrc(from htmlString: String) -> String? {
        let pattern = "<img[^>]+src=\"([^\"]+)\""
        
        if let regex = try? NSRegularExpression(pattern: pattern, options: []) {
            let range = NSRange(htmlString.startIndex..., in: htmlString)
            if let match = regex.firstMatch(in: htmlString, options: [], range: range) {
                if let srcRange = Range(match.range(at: 1), in: htmlString) {
                    return String(htmlString[srcRange])
                }
            }
        }
        return nil
    }
}

RSSItem.swift

RSS フィードをパースした結果のデータを管理するモデルクラスです

import Foundation
import SwiftUI

struct RSSItem: Identifiable {
    let id = UUID()
    let title: String
    let link: String
    let description: String
    let content: String  // html 付きの description
    let imageSrc: String?
}

ContentView.swift

SwiftUI で RSS フィードのモデル情報を表示します

import SwiftUI

struct ContentView: View {
    @StateObject private var rssFeedParser = RSSFeedParser()
    
    var body: some View {
        NavigationView {
            List(rssFeedParser.items) { item in
                VStack(alignment: .leading) {
                    Text(item.title)
                        .font(.headline)
                    if let imageSrc = item.imageSrc {
                        AsyncImage(url: URL(string: imageSrc)) { image in
                            image
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .frame(height: 200)
                        } placeholder: {
                            ProgressView()
                        }
                    }
                    Text(item.description)
                        .font(.body)
                    Text(item.link)
                        .font(.subheadline)
                        .foregroundColor(.blue)
                }
            }
            .navigationTitle("RSS Feed")
            .onAppear {
                if let url = URL(string: "https://figmanendoroiddb.blog.fc2.com/?xml") { // RSS URLをここに設定
                    rssFeedParser.loadRSSFeed(url: url)
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

testApp.swift

実行メインクラス

import SwiftUI

@main
struct testApp: App {  
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

最後に

SwiftUI を使った場合 UI とモデルのデータバインドがほぼ自動でやってくれるので楽です

表示するコンテンツの情報も SwiftUI 側で調整するだけで RSS フィードを取得する側のロジックでは全く気にする必要がないです

2024年10月23日水曜日

SwiftUI + BackgroundTasks で定期的にローカルプッシュを送信する方法

SwiftUI + BackgroundTasks で定期的にローカルプッシュを送信する方法

概要

アプリがバックグランドに移行した際にバックグランドタスクを使って定期的にローカルプッシュを送信してみます

環境

  • macOS 15.0.1
  • Xcode 16.0

Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>BGTaskSchedulerPermittedIdentifiers</key>
        <array>
                <string>com.example.app.refresh</string>
        </array>
        <key>UIBackgroundModes</key>
        <array>
                <string>fetch</string>
        </array>
</dict>
</plist>

サンプルコード

ポイントはタスクのイベントハンドラで (handleAppRefresh) で再キューイングしている部分とタスクが実行された際にローカルプッシュを送信している部分です

import SwiftUI
import BackgroundTasks

@main
struct testApp: App {
    // SwiftUI における AppDelegateの設定
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) {
            if scenePhase == .background {
                // バックグラウンドに入る際にタスクをスケジュール
                appDelegate.scheduleAppRefresh()
            }
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 通知の許可
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
            if granted {
                print("Notification permission granted")
            } else if let error = error {
                print("Notification permission error: \(error.localizedDescription)")
            }
        }
        // バックグラウンドタスクの登録、ここのコールバックメソッドは実際にタスクが実行される際に呼ばれます
        BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.app.refresh", using: nil) { task in
            self.handleAppRefresh(task: task as! BGAppRefreshTask)
        }
        return true
    }
    
    func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
        // 10秒後に実行するようにしているが実際に実行されるのはアプリがバックグランドに移動してからタスクが実行可能になった10秒後に実行される
        // バックグランドに移動してから10秒後ではないので注意
        request.earliestBeginDate = Date(timeIntervalSinceNow: 10)
        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
            print("Unable to schedule app refresh: \(error)")
        }
    }
    
    // 実際にタスクが実行された際にタスクの実行結果のハンドリングを行うメソッド
    func handleAppRefresh(task: BGAppRefreshTask) {
        task.expirationHandler = {
            print("Task expired")
        }
        let operation = BlockOperation {
            // ローカルプッシュを送信
            self.sendLocalNotification()
            print("Background task is running")
        }
        operation.completionBlock = {
            task.setTaskCompleted(success: !operation.isCancelled)
            print("Task completed: \(operation.isCancelled ? "Cancelled" : "Success")")
            // タスク完了後、次のタスクをスケジュール
            self.scheduleAppRefresh()
        }
        OperationQueue().addOperation(operation)
    }
    
    // ローカルプッシュ行うメソッド
    func sendLocalNotification() {
        let content = UNMutableNotificationContent()
        content.title = "Background Task"
        content.body = "This is a local notification from the background task."
        content.sound = .default
        
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
        
        UNUserNotificationCenter.current().add(request) { error in
            if let error = error {
                print("Error in scheduling local notification: \(error.localizedDescription)")
            }
        }
    }
}

最後に

iPhone 側のリソース状況にもよりますがだいたい 10分から15分に1回ローカルプッシュが来るようになります
あくまでもアプリがバックグランドにいる場合なのでキルされた場合などは来ないのでご注意ください

2024年10月21日月曜日

SwiftUI + BackgroundTasks を試してみた

SwiftUI + BackgroundTasks を試してみた

概要

SwiftUI で Delegate を定義してその中でバックグランド処理を定義します
アプリがバックグランドに移動するとタスクがキューイングされ実行されます

Swift の BackgroundTasks の最大のポイントは自分で実行する時間を制御できない点です
iOS 側のリソース状況によって実行時間がだいぶ変わるのですぐには実行されません

環境

  • macOS 15.0.1
  • Xcode 16.0

Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>BGTaskSchedulerPermittedIdentifiers</key>
        <array>
                <string>com.example.app.refresh</string>
        </array>
        <key>UIBackgroundModes</key>
        <array>
                <string>fetch</string>
        </array>
</dict>
</plist>

サンプルコード

実際にタスクが実行されるのは iOS 側の制御になるので指定の時間や指定の間隔で必ず実行されないことに注意です

import SwiftUI
import BackgroundTasks

@main
struct testApp: App {
    // SwiftUI における AppDelegateの設定
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .onChange(of: scenePhase) {
            if scenePhase == .background {
                // バックグラウンドに入る際にタスクをスケジュール
                appDelegate.scheduleAppRefresh()
            }
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // バックグラウンドタスクの登録、ここのコールバックメソッドは実際にタスクが実行される際に呼ばれます
        BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.app.refresh", using: nil) { task in
            self.handleAppRefresh(task: task as! BGAppRefreshTask)
        }
        return true
    }
    
    func scheduleAppRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: "com.example.app.refresh")
        // 10秒後に実行するようにしているが実際に実行されるのはアプリがバックグランドに移動してからタスクが実行可能になった10秒後に実行される
        // バックグランドに移動してから10秒後ではないので注意
        request.earliestBeginDate = Date(timeIntervalSinceNow: 10)
        do {
            try BGTaskScheduler.shared.submit(request)
        } catch {
            print("Unable to schedule app refresh: \(error)")
        }
    }
    
    // 実際にタスクが実行された際にタスクの実行結果のハンドリングを行うメソッド
    func handleAppRefresh(task: BGAppRefreshTask) {
        task.expirationHandler = {
            print("Task expired")
        }
        let operation = BlockOperation {
            print("Background task is running")
        }
        operation.completionBlock = {
            task.setTaskCompleted(success: !operation.isCancelled)
            print("Task completed: \(operation.isCancelled ? "Cancelled" : "Success")")
        }
        OperationQueue().addOperation(operation)
    }
}

最後に

今回のサンプルコードではアプリがバックグランドに移動するたびにタスクがキューイングされるので定期的に実行されるようなタスクではありません

そもそも iOS ではバックグランド処理はリソースを占有する処理になるため悪とされているので可能な限り使わないほうがいいのかなと思います

Debug -> Simulate Background fetch で実行して 15 分後くらいにデバッグログが表示されました

2022年3月25日金曜日

SwiftUI を試してみたのでメモ

SwiftUI を試してみたのでメモ

概要

SwiftUI のチュートリアルの1章を試してみたの成果物と感じたことをメモしておきます

環境

  • macOS 11.6.5
  • Xcode 13.1

感想

  • 開発は Vue や React のように宣言的に行える (状態が変わると自動で描画し直すなど)
  • コンポーネントごとに SwiftUI ファイルを作成できる
  • Stack を使いこなせるといい UI ができそう
  • 既存の UIKit から SwiftUI へのコンバートは頑張ればできるみたい
  • ライブプレビューは便利だがある程度スペックのあるマシンでないと重くなる

以下成果物です

ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            MapView().ignoresSafeArea(edges: .top).frame(height: 300)
            CircleImage().offset(y: -130).padding(.bottom, -130)
            VStack(alignment: .leading) {
                Text("現在地").font(.title)
                HStack {
                    Text("詳細1")
                    Spacer()
                    Text("詳細2")
                }.font(.subheadline).foregroundColor(.secondary)
                Divider()
                Text("現在地について").font(.title2)
                Text("説明")
            }.padding()
            Spacer()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

CircleImage.swift

import SwiftUI

struct CircleImage: View {
    var body: some View {
        Image("test").resizable().frame(width: 150, height: 150).clipShape(Circle()).overlay(Circle().stroke(Color.white, lineWidth: 4)).shadow(radius: 7)
    }
}

struct CircleImage_Previews: PreviewProvider {
    static var previews: some View {
        CircleImage()
    }
}

MapView.swift

import SwiftUI
import MapKit

struct MapView: View {
    @State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 35.6834875, longitude: 139.7608643), span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
    
    var body: some View {
        Map(coordinateRegion: $region)
    }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}

2022年2月15日火曜日

Swift でアプリのバージョン情報を取得する方法

Swift でアプリのバージョン情報を取得する方法

概要

アプリ自体にバージョンを記載したいときに使えます
Xcode で設定しているバージョンを取得します

環境

  • macOS 11.6.2
  • Xcode 13.2.1

サンプルコード

CFBundleShortVersionString を取得します

let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String

2022年2月9日水曜日

Framework not found GoogleMobileAds

Framework not found GoogleMobileAds

概要

ビルド時にバイナリがないと言われてエラーになります
対処方法を紹介します

環境

  • macOS 11.6.2
  • Xcode 13.2.1

ライブラリの再インストール

プロジェクト配下に移動して以下を実行します

  • rm -rf Pods
  • pod install

発生する原因

考えられる原因としては間違ってバイナリを削除してしまった場合です
バイナリのファイルが結構大きくディスクの掃除をしているときに大きいファイルを自動的に削除するようにしていると誤って削除してしまうことがありそうです

2022年2月4日金曜日

(Kingfisher) No exact matches in call to instance method 'setImage'

(Kingfisher) No exact matches in call to instance method 'setImage'

概要

Kingfisher が 6 -> 7 にバージョンアップしてみたら setImage の互換性がなくなりタイトルのエラーが出たので対応してみました

環境

  • macOS 11.6.2
  • Xcode 13.2.1
  • Kingfisher 7.1.2 (was 6.3.0)

元のコード

completionHandler の返り値を使ってエラーかどうかの判定などを行う感じでした

imageView.kf.setImage(with: url,
    completionHandler: {
        (image, error, cacheType, imageURL) in
        imageView.image = image
})

修正後のコード

completionHandler の帰り値は result のみになっておりこの結果を元に更に成功 or 失敗で分岐させて処理を書くような感じになっています

これまで使っていた image は value.image という感じで参照することができます

imageView.kf.setImage(with: url,
    completionHandler: {
        result in
    switch result {
    case .success(let value):
        imageView.image = value.image
    case .failure(_):
        print("failure")
    }
})

最後に

外部のライブラリを使っているとバージョンアップの際に非互換が発生することがあるのでこういった対応はどうしてもやらないといけなくなります

参考サイト

2021年8月18日水曜日

dismiss がコールされたときに元の画面でイベントをハンドリングする方法

dismiss がコールされたときに元の画面でイベントをハンドリングする方法

概要

iOS13 以降で仕様が変わっているようです presentationControllerDidDismiss を実装する必要があります

環境

  • macOS 11.5
  • Xcode 12.5.1

元の画面で実装するコード

import UIKit

extension HomeViewController: UIAdaptivePresentationControllerDelegate {
    // NewViewController から戻ったときに呼ばれる
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        // ここは好きなコードに書き換えて OK
        self.viewDidLoad()
    }
}

class HomeViewController: UIViewController {
    
    override func viewDidLoad() {
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if (segue.identifier == "toNextViewController") {
            let nextvc: NextViewController = (segue.destination as? NextViewController)!
            // 以下が重要
            nextvc.presentationController?.delegate = self
        }
    }
}

HomeViewController -> NextViewController の画面遷移は storyboard を使っているので prepare を実装しています

遷移先の画面で実装するコード

extension NextViewController {
    override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
        super.dismiss(animated: flag, completion: completion)
        guard let presentationController = presentationController else {
            return
        }
        if #available(iOS 13.0, *) {
            presentationController.delegate?.presentationControllerDidDismiss?(presentationController)
        } else {
            // Fallback on earlier versions
        }
    }
}

class NextViewController: UIViewController {

    @IBAction func backToHome(_ sender: Any) {
        self.dismiss(animated: true, completion: nil)
    }

}

dismiss するボタンを設置することを想定していますがスワイプで消しても同じです

最後に

iOS12 以下もサポートしている場合は viewWillDisappear を HomeViewController に実装しておいたほうがいいかもしれないです

参考サイト

2021年8月12日木曜日

UIButton で画像とテキストを両方表示する

UIButton で画像とテキストを両方表示する

概要

Storyboard で設定する方法を紹介します

環境

  • macOS 11.5
  • Xcode 12.5.1

設定方法

  • Title・・・好きなテキスト
  • Image・・・好きな画像を設定

これだけでできました Edge で Inset などを設定する必要があるようなのですが自分はそれを設定しないでもできました

文字色などが自動で変わることがあるのでその場合は背景と同色にならないように注意しましょう

2021年8月11日水曜日

RealmSwift はモデルだけ定義してあとは必要な箇所で RealmSwift を import するのが良さそう

RealmSwift はモデルだけ定義してあとは必要な箇所で RealmSwift を import するのが良さそう

概要

RealmSwift を扱う場合にモデルと CRUD 操作をどこに書くのがいいかなと悩んでいます 個人的にはシンプルに使いたいところで使う方式が一番しっくり来たので備忘録として残しておきます

環境

  • macOS 11.5
  • Xcode 12.5.1

モデルだけ定義する

Realm の基本的な使い方としてモデルを定義すると思うのですがそのモデル内では余計なことはしないようにします

単純にモデルだけを定義します

import RealmSwift

class BoardConfig: Object {
    @objc dynamic var name: String = ""
    @objc dynamic var point: Int = 0
}

View 側でモデルを使う

データの保存が取得は基本的に UIViewController などで行います view 側にも import RealmSwift が必要でかつデータにアクセスするために Realm() オブジェクトを生成する必要がありますが余計なことを考えなくて済むのでこの方法がベストだと思います

class BoardConfigViewController: UIViewController {
    override func viewDidLoad() {
        // データの保存
        let config = BoardConfig()
        config.name = "board1"
        config.point = 20
        let realm = try! Realm()
        try! realm.write {
            realm.add(config)
        }
        // データの取得
        let configs = realm.objects(BoardConfig.self)
        for config in configs {
            print(config)
        }
    }
}

上記では viewDidLoad で行っていますが Realm へのアクセスが必要な箇所で上記を呼び出す感じになります

Realm のオブジェクトを毎回生成するとあれなので Realm のオブジェクトはメンバー変数として扱っても良いかなと思います

アンチパターンっぽい書き方は

過去にモデルのクラスにデータを保存するメソッドやデータを取得するメソッドを作成していたのですがそれだとモデルを view 側でも扱うしモデル側でも扱う必要がありコードが重複している感がありました

CRUD 処理をまとめるという意味では良かったかもしれないのですがデータの変換などを考えると結局コードがごちゃごちゃするので微妙かなと思った感じです

モデルはあくまでもモデルでそれを view 側で変換したりするほうが結果的にキレイにコードになる印象です

最後に

結局シンプルなのが一番です

2021年8月10日火曜日

Asset Unavailable: SF Symbol 'gearshape.fill' is unavailable prior to iOS 14.0. Add a fallback image of the same name to the asset catalog for backward deployment.

Asset Unavailable: SF Symbol 'gearshape.fill' is unavailable prior to iOS 14.0. Add a fallback image of the same name to the asset catalog for backward deployment.

概要

iOS14では新しいシンボルを使うようになりました iOS14 以下のバージョンに対応したアプリを作成している場合に SF Symbols を使おうとするとタイトルの警告が出るようになります そんな場合の対処方法を紹介します

環境

  • macOS 11.5
  • Xcode 12.5.1

SF Symbols 3 アプリのダウンロードとインストール

gearshape.fill.svg が必要になります Apple が公式で作成しているシンボルのアプリがありそこから書き出しすることができるのでアプリをインストールします

https://developer.apple.com/sf-symbols/

dmg ファイルを開きアプリケーション配下に配置すればインストール完了です

gearshape.fill の検索

アプリを開いてシンボルを探しましょう 右上に検索があるのでそこから「gearshape.fill」と検索しましょう

シンボルを書き出す

シンボルが見つかったら目的の SVG を作成します メニューから「ファイル」->「シンボルを書き出す」を選択します

また書き出す際にはバージョンを「2」にしましょう 古いバージョンの svg でないと古い iOS では動作しないためです iOS14 しかサポートしないのであればバージョン3 でも大丈夫です

Assets.xcaseets に登録する

あとは書き出された svg を assets に登録すれば OK です アセットのタイプは「Symbol Image Set」を選択し名前は「gearshape.fill」にします

あとは svg をドラッグアンドドロップで登録すれば OK です

動作確認

再度ビルドしてアプリを動かしてみましょう 警告が消えるのが確認できると思います

2021年7月6日火曜日

Firebase の File Storage の使い方

Firebase の File Storage の使い方

概要

Firebase の File Storage を使ってみました ファイルのダウンロードをする際に少し気をつけることがありました インストール方法や初期化の部分は省略しています

環境

  • macOS 11.4
  • Xcode 12.5.1
  • Firebase/Storage 7.11.0

ローカルへのファイルのダウンロード

ポイントはファイルを保存できるパスが決まっているという点です 自分のアプリ配下の Documents にしか保存できないので注意しましょう

自分の Documents 配下を参照するためのプロパティがちゃんと提供されているのでそれを使うのが無難です

import Firebase

let storage = Storage.storage()
let storageRef = storage.reference()
let name = "test.txt"
let testfileRef = storageRef.child(name)
let fileManager = FileManager.default
let articlePath = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
let localURL = articlePath.appendingPathComponent(name)

_ = testfileRef.write(toFile: localURL) { url, error in
    if let error = error {
        // firebase からファイルの取得失敗
        print(error)
    } else {
        // ファイルのダウンロードに成功
	print("success")
    }
}

メタ情報の参照

例えばファイルの md5 ハッシュを取得するには以下のようにします 取得可能なメタデータの一覧はこちらにあります

fndbfileRef.getMetadata { metadata, error in
    if let error = error {
        // firebase のメタデータの取得に失敗
        print(error)
    } else {
      	// メタデータの取得に成功
        print(metadata?.md5Hash)
    }
}

参考サイト