SwiftUIは簡潔にUIを記述できる。100行内にUI付きのサンプルを記述できるのではないかという思いつきで100行(未満)でUI付きサンプルコードを記述できるのではないか?という発想で初めてみる。サンプルソースコードはGitGistで100行未満を目指すが説明はコメンタリー扱いで追記する。
端的にいうと
アプリが動作しているデバイスのアイコンを表示するサンプル。
1)Apple謹製シンボルリソースSF Symbols
Appleが提供しているアイコンリソース。フリーのシンボルリソース(iconmonstrなど)を使用せずに豊富なシンボルリソースを利用することができる。後悔はSwiftUI発表と同じ2019年iOS13SDKなのでSwiftUIのために用意したとも言えなくもない。アイコンリソースのピックアップはmacOS用に専用アプリが公開されている。
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はデバイスに対応したシンボルを見つける手助けをしてはくれない。
しようがないのでデバイス情報を取得できるミドルウェアを導入しアイコンイメージを特定する際に使用する。
DeviceGuruでハードウェア固有文字列からデバイスを特定し、FaceID対応のデバイスを特定することでiPhone, iPadとホームボタンの有無の組み合わせを判断できる。ハードウェア固有文字列には一定の規則がありinclude判別だけで多くはカバーできるが例外も存在しiPhone SE(2nd gen)だけはexluce判定が必要な特殊な機種となっている。
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 | |
} |
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モデファイヤーを使ってアイコンの大きさを調整している。
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() | |
} | |
} |
5) まとめ
100行未満サンプルコード企画は投稿内容のハードルを下げつつ定期的に投稿するための取り組みとして初めている。自分自身の備忘録としても有用なので今後の継続を検討したい。
今回の方法の改善余地としてはハードウェア文字列でアイコンを判別する方法はデバイスが増えると破綻するのでデバイス判別用のリストをクラウド上に配置して参照するなどで対処可能だろう。