能登 要

札幌在住のiOSアプリ開発者。SwiftUI により分割されたデバイス間を縦横にやりとりできる考え方に転換しています。

iOSアプリ開発者。2009年のiPhoneアプリ開発開始時期から活動。開発言語のアップデートの中でSwiftUIおよび周辺技術に着目中。

SwiftUI100行サンプルチャレンジ① - Apple謹製シンボルリソースSF Symbols2とデバイスアイコンを表示する - iOS アプリケーション開発

SwiftUIは簡潔にUIを記述できる。100行内にUI付きのサンプルを記述できるのではないかという思いつきで100行(未満)でUI付きサンプルコードを記述できるのではないか?という発想で初めてみる。サンプルソースコードはGitGistで100行未満を目指すが説明はコメンタリー扱いで追記する。

端的にいうと

アプリが動作しているデバイスのアイコンを表示するサンプル。

1)Apple謹製シンボルリソースSF Symbols

Appleが提供しているアイコンリソース。フリーのシンボルリソース(iconmonstrなど)を使用せずに豊富なシンボルリソースを利用することができる。後悔はSwiftUI発表と同じ2019年iOS13SDKなのでSwiftUIのために用意したとも言えなくもない。アイコンリソースのピックアップはmacOS用に専用アプリが公開されている。

SF Symbols - Apple Developer

2020年12月現在メジャーバージョンは2で、追加のシンボルと一部カラーシンボルが追加されている。SF Symbolsの振る舞いで興味深いのがテキストとの組み合わせ利用を前提とした設計で提供されており、a.ベースラインを備えている、b.ォントサイズと連動するなどの仕組みが用意されている。

2)SwiftUIでの利用方法

ImageでSF Symbolsに含まれるシンボルごとの名前を指定して使用できる。使用に関して初期化パタメーターが用意されている。

init(systemName: String)

SF Symbolsについて説明した際に、テキストとの組み合わせ利用前提の設計とSF Symbolsを紹介したがSwiftUIで利用した際に威力が発揮される。.fontや.wightモデファイヤーをImageに適用するとSF Symbolsアイコンサイズやweightを調整できる。

3)デバイスのアイコン判定

SF SymbolsにはApple製品のアイコンも多数集力されている。iPhoneならホームボタン付き、ノッチ付といったアイコンを入手できる。アプリが動作しているデバイスに対応したアイコンを表示する場合、Appleはデバイスに対応したシンボルを見つける手助けをしてはくれない。

しようがないのでデバイス情報を取得できるミドルウェアを導入しアイコンイメージを特定する際に使用する。

InderKumarRathore/DeviceGuru

DeviceGuruでハードウェア固有文字列からデバイスを特定し、FaceID対応のデバイスを特定することでiPhone, iPadとホームボタンの有無の組み合わせを判断できる。ハードウェア固有文字列には一定の規則がありinclude判別だけで多くはカバーできるが例外も存在しiPhone SE(2nd gen)だけはexluce判定が必要な特殊な機種となっている。

import SwiftUI
import DeviceGuru

enum DeviceStyleIcon: String {
    case phoneHome = "phh" // iPhone and iPod touch devices with homebutton
    case phone = "ph" // iPhone
    case padHome = "pah" // iPad witn homebutton
    case pad = "pa" // iPad
    case none = "n" // none (AppleWatch, appleTV, mac)
}

extension DeviceGuru {
    func styleIcon() -> DeviceStyleIcon {
        let notHaveHomeButton = self.notHaveHomeButton()
        let platform = self.platform()
        let deviceIconType: DeviceStyleIcon
        switch platform {
        case .iPhone, .iPodTouch:
            deviceIconType = notHaveHomeButton ? .phone : .phoneHome
        case .iPad:
            deviceIconType = notHaveHomeButton ? .pad : .padHome
        default:
            deviceIconType = .none
        }
        return deviceIconType
    }
    
    func notHaveHomeButton() -> Bool {
        let hardwareString = self.hardwareString()
        let includePrefixCollection = ["iPhone10", "iPhone11", "iPhone12", "iPhone13", "iPad8", "iPad13",]
        let excludePrefixCollection = ["iPhone12,8","iPhone10,1","iPhone10,2","iPhone10,4","iPhone10,5",]
        let notHaveHomeButton = includePrefixCollection.contains(where: { (includePrefix) -> Bool in hardwareString.hasPrefix(includePrefix) }) && !excludePrefixCollection.contains { (excludePrefix) -> Bool in hardwareString.hasPrefix(excludePrefix) }
        return notHaveHomeButton
    }
}

extension DeviceStyleIcon {
    func imageIcon() -> Image {
        switch self {
        case .phoneHome:
            return Image(systemName: "iphone.homebutton")
        case .phone:
            return Image(systemName: "iphone")
        case .padHome:
            return Image(systemName: "ipad.homebutton")
        case .pad:
            return Image(systemName: "ipad")
        default:
            return Image(systemName: "laptopcomputer")
        }
    }
}

struct ContentView: View {
    func imageIcon() -> Image {
        DeviceGuru().styleIcon().imageIcon()
    }
    func deviceDescription() -> Text {
        Text(DeviceGuru().hardwareDescription() ?? "")
    }
    var body: some View {
        GeometryReader{ geometry in
            HStack {
                Spacer()
                VStack {
                    Spacer()
                    self.imageIcon()
                        .font(.system(size: min(geometry.size.width, geometry.size.height) * 0.2, weight: .thin))
                        .foregroundColor(.black)
                        .padding(min(geometry.size.width, geometry.size.height) * 0.05)
                        .background(
                            Circle()
                                .foregroundColor(
                                    .white
                                )
                        )
                    self.deviceDescription()
                        .foregroundColor(.black)
                    Spacer()
                }
                Spacer()
            }
        }
        .background(Color(red: 0.8, green: 0.8, blue: 0.8))
        .ignoresSafeArea()
    }
}

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

    func notHaveHomeButton() -> Bool {
        let deviceGuru = DeviceGuru()
        let hardwareString = deviceGuru.hardwareString()
        let includePrefixCollection = ["iPhone10", "iPhone11", "iPhone12", "iPhone13", "iPad8", "iPad13",]
        let excludePrefixCollection = ["iPhone12.8",]
        let notHaveHomeButton = includePrefixCollection.contains(where: { (includePrefix) -> Bool in hardwareString.hasPrefix(includePrefix) }) && !excludePrefixCollection.contains { (excludePrefix) -> Bool in hardwareString.hasPrefix(excludePrefix) }
        return notHaveHomeButton
    }

4) サンプルコード

実際のコードは以下となる。前提としてDeviceGuruの導入が必要であるが、試すぐらいであればSwiftPackageManagerでプロジェクトに導入するのがお手軽だと思う。

取得したアイコンのサイズをGeometoryReader経由で調整している。調査委に際して.fontと.weightモデファイヤーを使ってアイコンの大きさを調整している。

// Failed to load gist 40663ec56c55fa296ea4b36eea8dbfc5: filename is required for raw URL fetch without GITHUB_TOKEN (use gist:id?file=filename or gist:id#filename syntax)

5) まとめ

100行未満サンプルコード企画は投稿内容のハードルを下げつつ定期的に投稿するための取り組みとして初めている。自分自身の備忘録としても有用なので今後の継続を検討したい。

今回の方法の改善余地としてはハードウェア文字列でアイコンを判別する方法はデバイスが増えると破綻するのでデバイス判別用のリストをクラウド上に配置して参照するなどで対処可能だろう。