20210226のiOSに関する記事は8件です。

SF SymbolsのsystemNameでタイポを防ぐために....

環境

  • Swift 5.3.2

実装

enum SFSymbol {
    case rectangleAndPencilAndEllipsis
    case ...

    var image: UIImage? {
        switch self {
        case .rectangleAndPencilAndEllipsis: return UIImage(systemName: "rectangle.and.pencil.and.ellipsis")
        case ...
        }
    }
}

let image = SFSymbol.rectangleAndPencilAndEllipsis.image

たったこれだけのことなのに、
よく分からない寄り道をしました。

その作業工程で
正規表現を使わずにlowerCamelCaseをsnake_case等に変換するStringのExtension
という副産物ができたので合わせてご覧ください。

経緯

SF Symbolsの画像を複数箇所で利用する場合

let image = UIImage(systemName: "rectangle.and.pencil.and.ellipsis")

毎回文字列指定するのはタイポ起きそうだし長いしなんだかなぁと思っていました。

過ち

その一

早速足を踏み外します。

enum SFSymbol {
    case rectangleAndPencilAndEllipsis
    case ...

    var name: String {
        switch self {
        case .rectangleAndPencilAndEllipsis: return "rectangle.and.pencil.and.ellipsis"
        case ...
        }
    }
}

let image = UIImage(systemName: SFSymbol.rectangleAndPencilAndEllipsis.name)

この時点でなんで気づかないんですかね。

その二

微調整が入ります。

enum SFSymbol {
    case rectangleAndPencilAndEllipsis
    case ...

    var name: String {
        switch self {
        case .rectangleAndPencilAndEllipsis: return "rectangle.and.pencil.and.ellipsis"
        case ...
        }
    }
}

extension UIImage {
    convenience init?(sfSymbol: SFSymbol) {
        self.init(systemName: sfSymbol.name)
    }
}

let image = UIImage(sfSymbol: .rectangleAndPencilAndEllipsis)

何がしたかったんですかね。

その三

過去の自分:「nameで返してる文字列も無くしてしまえばいいのでは?」

副産物が出来上がります。

extension String {
    var splitLowercaseStrings: [String] {
        guard self.includesUppercaseChar else { return [self] }
        let (lowercaseString, otherString) = self.dividedLowercaseStringBeforeUppercaseCharAndOther
        let replacedOtherString = otherString.replacedFirstCharWithLowercaseChar
        guard replacedOtherString.includesUppercaseChar else { return [lowercaseString, replacedOtherString] }
        return [[lowercaseString],
                replacedOtherString.splitLowercaseStrings]
            .flatMap { $0 }
    }

    private var includesUppercaseChar: Bool {
        filter { $0.isUppercase }.count != 0
    }

    private var dividedLowercaseStringBeforeUppercaseCharAndOther: (prefixString: String, otherString: String) {
        guard let uppercaseCharIndex = firstIndex(where: { $0.isUppercase }) else { return (self, "") }
        let targetRange = startIndex..<uppercaseCharIndex
        let firstLowercaseString = String(self[targetRange])
        var otherString = self
        otherString.removeSubrange(targetRange)
        return (firstLowercaseString, otherString)
    }

    private var replacedFirstCharWithLowercaseChar: String {
        var string = self
        let firstUppercaseChar = string.first!
        let firstLowercaseChar = firstUppercaseChar.lowercased()
        let secondIndex = string.index(after: string.startIndex)
        string.insert(contentsOf: firstLowercaseChar, at: secondIndex)
        return String(string.dropFirst())
    }
}

enum SystemSymbol: String {
    case rectangleAndPencilAndEllipsis
    case ...
}

let systemImageName = SystemSymbol.rectangleAndPencilAndEllipsis.rawValue.splitLowercaseStrings.joined(separator: ".")
let image = UIImage(systemName: systemImageName)

地獄ですね。

最終的に

そもそもSF Symbolsを複数箇所で利用していませんでした。

余談

副産物について

let ordinalNumbers = "firstSecondThirdFourthFifthSixth"

print("dot: \(ordinalNumbers.splitLowercaseStrings.joined(separator: "."))")
// dot: first.second.third.fourth.fifth.sixth
print("underScore: \(ordinalNumbers.splitLowercaseStrings.joined(separator: "_"))")
// underScore: first_second_third_fourth_fifth_sixth

lowerCamelCaseの文字列を任意の文字列で連結できます。

(※)先頭の大文字を小文字に差し替える
var replacedFirstCharWithLowercaseChar: String では、
replacingOccurrences(of:with:)も試しましたが、
後続している大文字始まりの文字列の塊があると、その大文字も差し代わってしまうので
insert(contentsOf: at:)dropFirst(_:)を用いて処理しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

xcode 上でios の devicetype を取得する

概要

devicetype を判定するための資料がうまく纏まっているサイトが少なかったので、
どなたか参考になる方がいるのを願って備忘録とします。

ここを参照

// Output on a simulator
@"i386"      on 32-bit Simulator
@"x86_64"    on 64-bit Simulator

// Output on an iPhone
@"iPhone1,1" on iPhone
@"iPhone1,2" on iPhone 3G
@"iPhone2,1" on iPhone 3GS
@"iPhone3,1" on iPhone 4 (GSM)
@"iPhone3,2" on iPhone 4 (GSM Rev A)
@"iPhone3,3" on iPhone 4 (CDMA/Verizon/Sprint)
@"iPhone4,1" on iPhone 4S
@"iPhone5,1" on iPhone 5 (model A1428, AT&T/Canada)
@"iPhone5,2" on iPhone 5 (model A1429, everything else)
@"iPhone5,3" on iPhone 5c (model A1456, A1532 | GSM)
@"iPhone5,4" on iPhone 5c (model A1507, A1516, A1526 (China), A1529 | Global)
@"iPhone6,1" on iPhone 5s (model A1433, A1533 | GSM)
@"iPhone6,2" on iPhone 5s (model A1457, A1518, A1528 (China), A1530 | Global)
@"iPhone7,1" on iPhone 6 Plus
@"iPhone7,2" on iPhone 6
@"iPhone8,1" on iPhone 6S
@"iPhone8,2" on iPhone 6S Plus
@"iPhone8,4" on iPhone SE
@"iPhone9,1" on iPhone 7 (CDMA)
@"iPhone9,3" on iPhone 7 (GSM)
@"iPhone9,2" on iPhone 7 Plus (CDMA)
@"iPhone9,4" on iPhone 7 Plus (GSM)
@"iPhone10,1" on iPhone 8 (CDMA)
@"iPhone10,4" on iPhone 8 (GSM)
@"iPhone10,2" on iPhone 8 Plus (CDMA)
@"iPhone10,5" on iPhone 8 Plus (GSM)
@"iPhone10,3" on iPhone X (CDMA)
@"iPhone10,6" on iPhone X (GSM)
@"iPhone11,2" on iPhone XS
@"iPhone11,4" on iPhone XS Max
@"iPhone11,6" on iPhone XS Max China
@"iPhone11,8" on iPhone XR
@"iPhone12,1" on iPhone 11
@"iPhone12,3" on iPhone 11 Pro
@"iPhone12,5" on iPhone 11 Pro Max
@"iPhone12,8" on iPhone SE (2nd Gen)
@"iPhone13,1" on iPhone 12 Mini
@"iPhone13,2" on iPhone 12
@"iPhone13,3" on iPhone 12 Pro
@"iPhone13,4" on iPhone 12 Pro Max

//iPad 1
@"iPad1,1" on iPad - Wifi (model A1219)
@"iPad1,2" on iPad - Wifi + Cellular (model A1337)

//iPad 2
@"iPad2,1" - Wifi (model A1395)
@"iPad2,2" - GSM (model A1396)
@"iPad2,3" - 3G (model A1397)
@"iPad2,4" - Wifi (model A1395)

// iPad Mini
@"iPad2,5" - Wifi (model A1432)
@"iPad2,6" - Wifi + Cellular (model  A1454)
@"iPad2,7" - Wifi + Cellular (model  A1455)

//iPad 3
@"iPad3,1" - Wifi (model A1416)
@"iPad3,2" - Wifi + Cellular (model  A1403)
@"iPad3,3" - Wifi + Cellular (model  A1430)

//iPad 4
@"iPad3,4" - Wifi (model A1458)
@"iPad3,5" - Wifi + Cellular (model  A1459)
@"iPad3,6" - Wifi + Cellular (model  A1460)

//iPad AIR
@"iPad4,1" - Wifi (model A1474)
@"iPad4,2" - Wifi + Cellular (model A1475)
@"iPad4,3" - Wifi + Cellular (model A1476)

// iPad Mini 2
@"iPad4,4" - Wifi (model A1489)
@"iPad4,5" - Wifi + Cellular (model A1490)
@"iPad4,6" - Wifi + Cellular (model A1491)

// iPad Mini 3
@"iPad4,7" - Wifi (model A1599)
@"iPad4,8" - Wifi + Cellular (model A1600)
@"iPad4,9" - Wifi + Cellular (model A1601)

// iPad Mini 4
@"iPad5,1" - Wifi (model A1538)
@"iPad5,2" - Wifi + Cellular (model A1550)

//iPad AIR 2
@"iPad5,3" - Wifi (model A1566)
@"iPad5,4" - Wifi + Cellular (model A1567)

// iPad PRO 9.7"
@"iPad6,3" - Wifi (model A1673)
@"iPad6,4" - Wifi + Cellular (model A1674)
@"iPad6,4" - Wifi + Cellular (model A1675)

//iPad PRO 12.9"
@"iPad6,7" - Wifi (model A1584)
@"iPad6,8" - Wifi + Cellular (model A1652)

//iPad (5th generation)
@"iPad6,11" - Wifi (model A1822)
@"iPad6,12" - Wifi + Cellular (model A1823)

//iPad PRO 12.9" (2nd Gen)
@"iPad7,1" - Wifi (model A1670)
@"iPad7,2" - Wifi + Cellular (model A1671)
@"iPad7,2" - Wifi + Cellular (model A1821)

//iPad PRO 10.5"
@"iPad7,3" - Wifi (model A1701)
@"iPad7,4" - Wifi + Cellular (model A1709)

// iPad (6th Gen)
@"iPad7,5" - WiFi
@"iPad7,6" - WiFi + Cellular

// iPad (7th Gen)
@"iPad7,11" - WiFi
@"iPad7,12" - WiFi + Cellular

//iPad PRO 11"
@"iPad8,1" - WiFi
@"iPad8,2" - 1TB, WiFi
@"iPad8,3" - WiFi + Cellular
@"iPad8,4" - 1TB, WiFi + Cellular

//iPad PRO 12.9" (3rd Gen)
@"iPad8,5" - WiFi
@"iPad8,6" - 1TB, WiFi
@"iPad8,7" - WiFi + Cellular
@"iPad8,8" - 1TB, WiFi + Cellular

//iPad PRO 11" (2nd Gen)
@"iPad8,9" - WiFi
@"iPad8,10" - 1TB, WiFi

//iPad PRO 12.9" (4th Gen)
@"iPad8,11" - (WiFi)
@"iPad8,12" - (WiFi+Cellular)

// iPad mini 5th Gen
@"iPad11,1" - WiFi
@"iPad11,2" - Wifi  + Cellular

// iPad Air 3rd Gen
@"iPad11,3" - Wifi 
@"iPad11,4" - Wifi  + Cellular

// iPad (8th Gen)
@"iPad11,6" - iPad 8th Gen (WiFi)
@"iPad11,7" - iPad 8th Gen (WiFi+Cellular)

// iPad Air 4th Gen
@"iPad13,1" - iPad air 4th Gen (WiFi)
@"iPad13,2" - iPad air 4th Gen (WiFi+Cellular)

//iPod Touch
@"iPod1,1"   on iPod Touch
@"iPod2,1"   on iPod Touch Second Generation
@"iPod3,1"   on iPod Touch Third Generation
@"iPod4,1"   on iPod Touch Fourth Generation
@"iPod5,1"   on iPod Touch 5th Generation
@"iPod7,1"   on iPod Touch 6th Generation
@"iPod9,1"   on iPod Touch 7th Generation

// Apple Watch
@"Watch1,1" on Apple Watch 38mm case
@"Watch1,2" on Apple Watch 38mm case
@"Watch2,6" on Apple Watch Series 1 38mm case
@"Watch2,7" on Apple Watch Series 1 42mm case
@"Watch2,3" on Apple Watch Series 2 38mm case
@"Watch2,4" on Apple Watch Series 2 42mm case
@"Watch3,1" on Apple Watch Series 3 38mm case (GPS+Cellular)
@"Watch3,2" on Apple Watch Series 3 42mm case (GPS+Cellular)
@"Watch3,3" on Apple Watch Series 3 38mm case (GPS)
@"Watch3,4" on Apple Watch Series 3 42mm case (GPS)
@"Watch4,1" on Apple Watch Series 4 40mm case (GPS)
@"Watch4,2" on Apple Watch Series 4 44mm case (GPS)
@"Watch4,3" on Apple Watch Series 4 40mm case (GPS+Cellular)
@"Watch4,4" on Apple Watch Series 4 44mm case (GPS+Cellular)
@"Watch5,1" on Apple Watch Series 5 40mm case (GPS)
@"Watch5,2" on Apple Watch Series 5 44mm case (GPS)
@"Watch5,3" on Apple Watch Series 5 40mm case (GPS+Cellular)
@"Watch5,4" on Apple Watch Series 5 44mm case (GPS+Cellular)
@"Watch5,9" on Apple Watch SE 40mm case (GPS)
@"Watch5,10" on Apple Watch SE 44mm case (GPS)
@"Watch5,11" on Apple Watch SE 40mm case (GPS+Cellular)
@"Watch5,12" on Apple Watch SE 44mm case (GPS+Cellular)
@"Watch6,1" on Apple Watch Series 6 40mm case (GPS)
@"Watch6,2" on Apple Watch Series 6 44mm case (GPS)
@"Watch6,3" on Apple Watch Series 6 40mm case (GPS+Cellular)
@"Watch6,4" on Apple Watch Series 6 44mm case (GPS+Cellular)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcodeを使ってipaをiPhoneにインストールする

対象

以下に当てはまる人

  • adhoc用などで書き出したipaファイルを配布するまえに実機で確認したい
  • ipaインストールのために余計なソフトを導入したくない
  • 以前はiTunesでできたらしいが今はできなくなって困っている

手順

Xcodeを開き、メニューバーからWindow > Devices and Simulatorsを開く
スクリーンショット 2021-02-26 9.58.14.png

INSTALLED APPSの下にある+をクリックする
スクリーンショット 2021-02-26 9.57.09.png

インストールしたいipaファイルを選択肢してopenをクリック
スクリーンショット 2021-02-26 9.57.31.png

インストールが始まります。

後書き

普通に初歩的なことだと思うけど、調べてもこれに関する情報が見つからなかった。
appleの純正ソフトだとApple Configurator2というのを使えばできるらしい。

それ以外だと有料ソフトを使うとか、脱獄するとかスマートではない方法ばかりヒットした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS14】アプリにウィジェット(WidgetKit)を導入してみた

watnowテックカレンダー2020/2/26】

今日はwatnowのryotaが担当します
よろしくお願いします

 はじめに

2020年9月にiOS14がリリースされ、その追加機能の一つとしてウィジェットという機能が利用可能になりました。

Apple公式のWidget Kitについて
Apple Introducing WidgetKit

ウィジェットはアプリの一部情報をiOSのホーム画面へ表示させられる機能です。この機能を用いればアプリを開かずにちょっとした情報を確認することができます。またウィジェットに任意の画面へのリンクを設定することもできユーザのアプリの利用率も向上させられます。
一方でWidgetKitを作成するにはswiftUIで実装する必要があり、なかなか導入しにくいと思う部分もあると思いますが、やってみると簡単に作成することができたので共有しようと思います。
image.png

Widget Kitを導入する

まずWidget Kitを導入するにあったってQ&Aです。(主に僕が思ってたことです)

  • Swiftでプロジェクトを作ってるけど、SwiftUIのWidget Kitってそもそも共存できるの?

    • SwiftUIはSwift言語で使用できるUI構築フレームワークということもあり問題なく共存できます。
      ただしWidget KitはiOS14以降のみの対応のため、それより以前のバージョンの端末では使用できない点に注意してください。
  • Swiftしか触ったことないけどSwiftUIでコードを書くのはちょっと..

    • Swiftを書いたことのある方にとってSwiftUIはなんとなくであれば実装しやすい言語だと思います。swiftUIではソースコードと、iPhone画面を同時に表示するプレビュー機能(名称はCanvas)があるので簡単にViewを確認できます。Widget Kitを導入する場合でもそこまで複雑な処理やUIを実装しない限り、実装するコードは少なくてすみます。
  • Widgetにメモリ制限とかはあるの?

    • Widget Kitではホーム画面に常時設置されるという特性上、厳しいメモリ制限があるようです。トータルで最大30MB以上のデータはロードできないといった報告があるようです。似たような例としてApp extensionsを実装したこちらの記事によると、使用する機種によって使用できるメモリ量も増減するようです。いずれにせよ複雑なグラフの描画や、解像度の高いデータの取得などは行わない方が良さそうです。
    • メモリ制限とかについて
      10 Tips on Developing iOS 14 Widgets

もしSwiftUIを初めて使うという方はまずは、公式のチュートリアルから始めるとわかりやすいです。

Appleが掲載している公式のSwiftUIチュートリアル
* Introducing SwiftUI

上記のSwiftUIチュートリアルを日本語で解説されているページもあります
* 【第1回】日本語版SwiftUIチュートリアル【基本要素を学ぶ】

導入

まずはWidgetKitのデータの取り扱い方についてはこちらの記事が非常にわかりやすくまとまっているので概要を掴むとわかりやすいです。
【iOS14】Widget(WidgetKit) まとめ

Widget Kitを導入していきます。
プロジェクトを作成したあとに、File=>New=>Target
image.png
真ん中らへんにWidget Extensionがあるのでクリックし、Next
image.png
Product Nameを適当に決めてFinish
image.png
するとProduct Name以下にこのようなファイルが出来上がっていると思います。
それぞれの役割として

  • widgetDemo.swift
    • =>データやViewとかの処理をまとめて行う。追加時には時刻を表示する場合のサンプルコードが記入されている。もし独自のウェイジェットを実装していくならViewとかでファイルを分けるとわかりやすい。
  • widgetDemo.intendefinition 
    • => メインのアプリとデータを共有したりするときにここで設定する
  • Assets.xcassets
    • => 静的な画像データとかはここにおく
  • info.plist 
    • => Widgetの設定プロパティ

image.png
以下のようにWidgetExtensionのViewが表示されています。

image.png
この状態で アプリのビルドを行い、ホーム画面を長押し=>widgetを追加する
これでホーム画面に現在の時間が表示されるウィジェットが追加されます。時計を表示させるだけなら実はこれだけで実装となります。
image.png
Simulator Screen Shot - iPod touch (7th generation) - 2021-02-26 at 11.59.45.png

APIからデータをとってきて表示させる

ここまでで、すでにホーム画面にWidgetを追加して、時間を表示させることができました。

ここをベースに開発していくことでアプリ側からデータを受け取って表示させたりサーバからデータを取得して表示させたりすることができます。
では今回はAPIからデータをとってきて表示させるWidgetを開発しようと思います。

完成はこんな感じです。今回はNewsAPIからJSONで技術系のニュースデータを取得してその1つめのデータをWidgetに追加するようにします。
Simulator Screen Shot - iPod touch (7th generation) - 2021-02-26 at 17.47.27.png

まずはニュースを取得するためにNewsAPIを利用するのでそのAPIKeyを取得します。News系のAPIで探してみるとこれが多く使われてるみたいです。APIKeyを取得したら以下のようにGETするとレスポンスがかえってきます。

http://newsapi.org/v2/top-headlines?sources=techcrunch&apiKey=APIキー

WidgetKitがHTTP通信を行うことができるようにinfo.plistを編集してあげる必要があります。HTTPS通信が必須化しているためHTTP通信を行うには許可する必要があります。
今回編集するのはアプリのinfo.plistではなく、Widgetのフォルダに作成されたinfo.plistを編集する必要があることに注意してください。

HTTP通信の許可する方法についてはこちらが参考になります。
iOSアプリのhttp通信を許可する方法

image.png

まずはCodableなモデルを作成していきます。

JSONからモデルを作成する時は、Convert JSON into gorgeousが便利です。JSONを入力するだけでいい感じにモデルを作ってくれます。

import WidgetKit
import SwiftUI
import Intents



import Foundation

// MARK: - News
struct News: Codable {
    let status: String?
    let totalResults: Int?
    let articles: [Article]?
}

// MARK: - Article
struct Article: Codable {
    let source: Source?
    let author, title, articleDescription: String?
    let url: String?
    let urlToImage: String?
    let publishedAt: String?
    let content: String?

    enum CodingKeys: String, CodingKey {
        case source, author, title
        case articleDescription = "description"
        case url, urlToImage, publishedAt, content
    }
}

// MARK: - Source
struct Source: Codable {
    let id: ID?
    let name: Name?
}

enum ID: String, Codable {
    case techcrunch = "techcrunch"
}

enum Name: String, Codable {
    case techCrunch = "TechCrunch"
}

次はWidgetKitのメインとなる部分です。今回は最初からあるSimpleEntryにnewsDataの項目を追加しています。取得したデータからTimelineEntryを作成し、それをもとにTimelineを構成していくイメージです。


import WidgetKit
import SwiftUI
import Intents


struct Provider: IntentTimelineProvider {
    @ObservedObject var NewsStore = SessionStore()
    func placeholder(in context: Context) -> SimpleEntry {
        #Widgetが読み込み中の時に呼ばれる
        SimpleEntry(date: Date(), configuration: ConfigurationIntent(), newsData:  [] as? News)
    }

    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
         #Widgetを追加しようとするときに例として表示される
         #ダミーのデータをいれるとよい
        let entry = SimpleEntry(date: Date(), configuration: configuration, newsData:  [] as? News)
        completion(entry)
    }

    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        /// 1時間ごとに更新する
        let refresh = Calendar.current.date(byAdding: .hour, value: 1, to: Date()) ?? Date()
        NewsStore.fetch{ newData in
            entries.append(SimpleEntry(date: Date(), configuration: ConfigurationIntent(), newsData: newData))

            let timeline = Timeline(entries: entries, policy: .after(refresh))
            completion(timeline)
        }
    }
}

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: ConfigurationIntent
    let newsData:News?

}

struct newsWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {

        VStack{
            URLImageView(url: URL(string: entry.newsData?.articles?[0].urlToImage ?? "")!)
                .aspectRatio(contentMode: .fill)
                Text(entry.newsData?.articles?[0].title ?? "")
                    .foregroundColor(.black)
                    .font(.headline)
                    .padding(8)
        } 
    }
}

@main
struct newsWidget: Widget {
    let kind: String = "newsWidget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            newsWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("News Widget")
        .description("News Widgetの説明")
        #大きなwidgetのみ対応する
        .supportedFamilies([.systemLarge])
    }
}

struct newsWidget_Previews: PreviewProvider {
    static var previews: some View {
        newsWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), newsData: [] as? News))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

データを取得するクラスはこんな感じで実装しています。

import Foundation
import Combine

final class SessionStore: ObservableObject {
    @Published  var current : News?
    init(){
        self.fetch{ val in
            print(val)
        }
    }
}
extension SessionStore{
    func fetch(completion : @escaping(News)->()){
        NewsClient.fetchSummary {
            self.current = $0
            completion($0)
        }
    }
}

最後にURLから画像を読み込む処理についてです。
Swiftで非同期で画像を取得するときはKing Fisherとかが便利なんですが、今回はSwiftUIで実装するということもあり処理を別途書く必要があります。

import SwiftUI
struct URLImageView: View {
    let url: URL
    @ViewBuilder
    var body: some View {
        if let data = try? Data(contentsOf: url), let uiImage = UIImage(data: data) {
            Image(uiImage: uiImage)
                .resizable()

        } else {
            Image(systemName: "photo")
                .resizable()
        }
    }
}

以上で、Widgetの実装は終わりになります。
これでWidgetを追加すると最新のニュースが表示されます。

今回使用したサンプルプロジェクトは以下になります。
https://github.com/ryota2425/widgetExample

最後に

WidgetKitはSwiftUIでの実装で難しそうに見えますが、ぜひチャレンジしてみてください。
今回はAPIの情報を表示させるだけでしたが、これを発展させてクリックした場にアプリで記事を表示させるとより実用的になっていくかと思います!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

daeファイル作成時に気を付けること

Mayaでdaeファイルをexportすると、ソフトウェアによってはblendShapeが読み込まれない。 exportしたdaeファイルをテキストエディタ等で開いて、「RELATIVE」を「NORMALIZED」にすべて書き換えると使えるようになる。 また、ピボットポイントを変更してもそのままだと反映されないので、修正→ピボットをベイク処理をしておく必要がある
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【SwiftUI】NavigationViewの基本的な使い方(前編)

前回に引き続き、アプリに使用した技術をアウトプットしていきます。
解説する私のアプリはこちら。
わかりすと -勉強サポートアプリ-

NavigationView

階層的な画面遷移を管理するビュー。

公式リファレンス

NavigationLink

NavigationView内で、画面遷移を行うトリガーになるビュー遷移先のビューの両方を指定できるビュー。NavigationViewとセットで使う。

公式リファレンス

実際の動作

※拡大推奨

動画のポイント

  • NavigationViewの外側の領域は切り替わらず、遷移先にも表示される。(動画内のヘルプボタン)
  • 遷移先の画面の左上には、前の画面にもどるためのボタンが表示される。
  • もどるボタンにはタイトルがそのまま書かれる。

コード

今回作成したサンプルアプリのソースコードはこちら

まず、一番上の階層から見ていきましょう。

ContentView.swift
import SwiftUI

struct ContentView: View {

    let regions = ["カントー地方", "ジョウト地方", "ホウエン地方", "シンオウ地方"]

    var body: some View {
        VStack{
            // ここから
            HStack{
                Spacer()
                Button(action: {}, label: {
                    Text("ヘルプ")
                })
            }
            .padding(.horizontal)
            // ここまではNavigationViewに含まれない

            //ここから
            NavigationView {
                List(0..<4) { n in
                    NavigationLink(regions[n], destination: Region(id: n, regionName: regions[n]))
                }
                .navigationTitle("そらをとぶ")
            }
            //ここまでがNavigationViewの領域
        }
    }
}

基本として、遷移する(切り替わる)のはNavigationViewの{}内の要素だけです。上のソースコードの「ヘルプ」というラベルのボタンはNavigationViewに含まれないため、リンクをタップしても切り替わらずに画面上部に残っています(動画を確認してください)。今回の「ヘルプ」ボタンのように、何らかの理由で遷移先にもずっと表示させたいものがある場合はNavigationViewに含めない方が良いでしょう

次にNavigationViewの中を見ていきます。

NavigationViewの中にはNavigationLinkが書かれており、Listの繰り返し処理で4つ並ぶようにしています。上で説明したとおり、NavigationLinkは画面遷移のトリガーと遷移先のビューを指定できます。今回はregions[n](配列regionsのn番目の要素)がトリガー、Regionビューが遷移先となっています。

Regionビューのソースコードはこちら。

Region.swift
import SwiftUI

struct Region: View {

    let id: Int //地方によって表示を変えるためのプロパティ
    let regionName: String //地方の名前をタイトルにするためのプロパティ

    var body: some View {
        //NavigationViewは書かない
        List(0..<4) { n in
            NavigationLink(regionsArray[id][n], destination: Flight(city: regionsArray[id][n]))
                .navigationTitle(regionName)
        }
    }
}

//各地方ごとのデータ
let kanto = ["マサラタウン", "トキワシティ", "ニビシティ", "ハナダシティ"]
let joto = ["ワカバタウン", "ヨシノシティ", "キキョウシティ", "ヒワダタウン"]
let hoen = ["ミシロタウン", "コトキタウン", "トウカシティ", "カナズミシティ"]
let shino = ["フタバタウン", "マサゴタウン", "クロガネシティ", "コトブキシティ"]

//idに対応する配列を取り出すための配列
let regionsArray = [kanto, joto, hoen, shino]

ここで見てほしいのは、NavigationLinkはあるのにNavigationViewの記述がどこにも書かれていないことです。NavigationViewは一番上の階層となるビューに書くだけで、遷移先にも機能を持たせることができます。

逆に、下の階層にもNavigationViewを書き、次の画面に遷移するとこのようになります。

スクリーンショット 2021-02-26 14.06.58.png

画面左上に、前の画面にもどるためのボタンが2つ並んでいることがわかると思います。これはNavigationViewの中にNavigationViewを作っていると判断された結果です。このような表示にしたい場合は別ですが、基本は一番上の階層にのみNavigationViewを書くことをおすすめします

次回はnavigationTitleについて書きます。

参考文献

SwiftUI徹底入門 金田浩明(著)(【注意】Xcode12非対応のものです)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PoCでiOSアーキテクチャにMVPを採用した話

TL;DR

PoC(検証)のための新規iOSアプリ開発で、iOSアプリ開発初心者がアーキテクチャにMVPを採用した理由と、MVPを実装するする上で気づいたことを共有するものです。

結論

  • アーキテクチャの検討に当たっては、①開発要件に合うものの中で、②経験があるor経験がなくても最も理解しやすそうな、設計パターンをサクッと決定するのがよさそうです
  • MVPのアーキテクチャパターンにおいては、プレゼンテーションロジックの分離が重要で、その際に活用できるのがProtocol

この記事の効能

  • 新規のiOSアプリ開発でアーキテクチャを決定する一助になるかもしれません
  • MVPの実装において筆者が大切だと思った、「プレゼンテーションロジックの分離」について理解が深められます

MVPの選定

手順

今回はデザイナーさんが作成してくださったワイヤーフレームがあったのでそれを参考にしながら行いました
1. 開発する画面を列挙する
2. 各画面や画面間で管理する状態を書き出す
3. 上記のステップでまとめた開発要件を元に設計パターンを考える

(例)画面・状態を書き出している様子
- 管理すべき状態
  - 画面をまたぐもの
    - セッション(ログイン)情報
 - サインアップ画面
    - IDとパスワードが不正の状態(validationに利用)
  - カート画面
   - カート内のアイテム
   - 合計金額

etc.

今回の前提条件

a. ページ間にまたがる状態はログインの有無だけで、状態管理が複雑でない
b. 検証用のiOSアプリで1+α人程度での小規模な開発
c. 開発者はiOSアプリ開発初心者

選択肢

MVC, MVP, MVVM, Flux, Redux

上記の全てについて一通り把握し検討を行いましたが、前提条件のaにあるように複雑な状態管理がないことから、MVCとMVPに絞りました。また他の設計パターンでは、導入に当たって外部のライブラリを使用すべきであったことも選定対象から外れた理由となりました。(例:MVVMにおけるRxSwiftなど)
MVCとMVPの2つの中から選定した理由としては、①決済などテストすべき項目を扱うためテストしやすい方がいい、②MVCから発展したものだからMVPの方が使いやすい(はず)になります。

アーキテクチャパターン選定のまとめ

結構時間をかけて真剣に設計パターンの検討を行いましたが、後から見返すとやってみないと分からないことも多いなと思います。また、実際に開発を進めて考えていく中でやっと理解が進むものもあります。結論として、アーキテクチャの検討に当たっては、①開発要件に合うものの中で、②経験があるor経験がなくても最も理解しやすそうな、設計パターンをサクッと決定してしまうことが良さそうかなと思っています。(BFFの設計も考えたことがあり、その経験も踏まえて。)
今回に関して言うと、ページをまたがる複雑な状態管理が必要なく、少人数での開発という開発要件で、比較的単純な(だと感じた)MVPを採用しました。

MVPにおけるプレゼンテーションロジック分離の重要性

MVPについて

MVPの中にも、データの流れによってPassive ViewSupervising Controllerという2つの方式があります。
Passive ViewはViewとModelのデータのやり取りにおいて必ずPresenterを経由するもので、データの流れが理解しやすく、全てのプレゼンテーションロジックのテストが可能です。一方でSupervising Controllerは、Model→Viewの流れにおいて、更新の通知により変更を知らせることができ、Presenterを挟まない分冗長なデータの流れを回避することができます。

今回は、テストが容易な点と開発の簡潔な点を重視してPassive Viewを採用しました。
それぞれの詳しい図解と解説は以下の記事が分かりやすかったです。(ただ、Supervising Controllerについては以下の記事ではData bindingになっていますが、参考文献のようにオブザーバー同期を使うものもあります)
StackOverFlowの「MVPとMVCの違い」についての回答を読んでみた

Passive Viewにおける各コンポーネントの役割

コンポーネント 役割
Model コマンドを受けて自身を更新(Presenterからのみアクセスされ、Viewとは直接の関わりをもちません)
View ユーザーの入力を受け、Presenterに伝える(Presenterからの描画指示に従うだけで、完全に受け身な立ち位置になります)
Presenter ViewからのコマンドをModelに送る・Modelからの変更を受け取り、Viewを更新(すべてのプレゼンテーションロジックを受けもちます)

参考文献より)

補足:プレゼンテーションロジックとは
「View(ユーザーの入力など)やModel(データの変更など)の変更から、画面の更新・遷移を行うロジックのこと」

Protocolの活用によるプレゼンテーションロジックの分離

実装している中で、プレゼンテーションロジックがViewに紛れていることに気付きました。今回はiOSアプリでよく用いられるViewControllerというファイルにViewの役割を持たせたのですが、ViewControllerのファイルの中に直接プレゼンテーションロジックを書き込んでいたのです。これはひとえに、各コンポーネントの役割を完全に理解できていなかったことが原因であると考えます。そこで以下の記事を見つけ、Protocolの活用法に気づくと共に、リファクタを行なってプレゼンテーションロジックをViewControllerから取り除きました。
MVP VS MVVM in iOS using swift

実装に当たっては、上記の記事よりもPresenterの役割が分かりやすいコードがあったのでそちらを参考にしました。
以下が参考にしたコードです。(参考文献で扱われているコードです。)

ポイントは、protocolの定義において、ユーザーからの入力(SearchUserPresenterInput)と、画面更新・遷移(SearchUserPresenterOutput)を分けて行うことで、それぞれの役割を明確にしている部分です。

MVPにおいてPresenterを実装するということ

簡単なことではありますが、気づくのに時間がかかったポイントを挙げます。

「全てのプレゼンテーションロジックを受けもつPresenterを実装する」ということは、

  • プレゼンテーションロジックは全てPresenterから呼び出されるべき(=Viewが呼び出すことはなく、また当然Viewに直接プレゼンテーションロジックを記述することもない)
  • ユーザーからの入力はInputProtocol、画面の描画はOutputProtocolで定義し、OutputProtocolの実装自体はViewControllerで継承して行うのがよい

ということであると理解しました。

(追記)この記事にいただいたご質問・ご意見

MVPは難しくないの?

参考文献では、MVCの責務を再分割したもの(MVCの三要素を60°回転したもの)であると言及されていたため、そこまで複雑なものではないと考えていました。
また以下の記事でも、MVVMやReduxと比べれば、規模を小さく開発できると考えられます。
手を動かしてMVPパターンを体験する

自分としては、記事で説明したようにPresenterを正しく理解できないなどのポイントはあるものの、小規模開発にも適したパターンであると考えます。今回、iOSアプリ開発が初めてであったことを考えると、MVC(Cocoa MVC)の習得にも学習コストは存在するので、MVPとそれほど差異はないのではないかと考えています。

MVCとの違いは?

MVCに対するMVPの利点として挙げられるのは、テストの容易性と作業分担のしやすさになります。これはPresenterという役割を作り、プレゼンテーションロジックを取り出したことによるものかと思われます。
特にPassive Viewの場合は、データの流れが単一なので実装に迷うことがないというメリットもあると、実装する中で感じました。

AndroidにもMVPはある?

あるようです。
Architecture of Android Apps

また、Androidには公式のアーキテクチャガイドがあるようで、素敵だなと思いました。
アプリ アーキテクチャ ガイド

参考

iOSアプリ設計パターン入門
iOSアプリ開発に限らず、設計パターンを学びたい方に損得抜きで強くおすすめです。歴史的経緯も含めた設計パターンの丁寧な解説、さらに各設計パターンのサンプルコードで構成されています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【NoCode】5年目iOSエンジニアが、アプリのE2Eテスト自動化サービスを作った話

iOSエンジニア歴も早5年目となりました。

その経験を活かして、iOSアプリのテスト自動化サービス「SmartQA」を作りました!

今回の開発では、Firebaseが大活躍してくれました。

SmartQAをリリースする上で、AppiumやFirebaseの技術的な知見をここに残せたらと思います。

概要

cover-icon.png

SmartQA - E2Eテスト自動化をノーコードで

ビルドをアップロードするだけで、ブラウザ上で簡単にテストの自動化ができます。

image.png

UIが変わってしまった場合でも、要素探索を自動で修復してくれます。

複数端末対応しているので、「iPhone SEiPadだとクラッシュしてしまった」なんてことも検知できます。

現在はiOSアプリのみ対応しております。

私について

10歳の頃にアセンブリに出会ったのがきっかけで、プログラミングの世界にのめり込みました。

どんなプログラムも足し算・引き算・比較・ジャンプで動いてることに感動したことを今でも覚えています。

京都大学工学部情報学科を卒業して、Levetty株式会社を立ち上げました。

在学中にiOSエンジニアとして働き始めました。

当時京都では学生がエンジニアとして働ける会社がほとんどなく、大学生協のアルバイト募集に稀に出現するエンジニア募集を見つけるしかありませんでした。

IMG_6460.JPG

大学生協を毎日クロールするスクリプトを組んで、LINEに通知して最速で応募したりしてました。

開発体制

開発期間としては、9月に実現できるのか技術検証を行い、10月から開発を始めました。

基本的に僕1人で開発を進めていたのですが、仲の良いエンジニア3人に手伝ってもらえたのがすごく大きかったです。

設計を綺麗にしてくれたり、インフラ整えてくれたり、なかなか手が回らない部分の実装をしてくれたり、サービスのアイデアをいただけたり。

とても感謝しています。

使用した技術

Appium

iOSやAndroidでシミュレータを操作するのに必要なライブラリです。

iOSで使う場合は、内部でXCUITestを使用しています。

Firebase

  • Authentication
  • Cloud Firestore
  • Cloud Functions
  • Cloud PubSub
  • Cloud Storage
  • Hosting

React.js / Redux / TypeScript

フロントは、SPA(シングルページアプリケーション)で作成しました。

Material-UIが大活躍してくれました。

Express / Node.js / TypeScript

テスト実行サーバー、macOS上で動作する簡易的なサーバーは全てExpressで実装しました。

AWS CDK / Ansible

サーバー側の実装は、Cloud Functionsに寄せていました。

しかし、540秒の実行時間の制限がきつく、テスト実行処理はAWSのEC2で運用することになりました。

手伝ってくれている凄腕エンジニアがAWS CDKAnsibleを使って構築してくれていました。

macOSを何台も用意する必要があるので、Ansibleを触ったのですが、革命を感じました。

CDKについては、僕は触ってないので分かりません、、。が、めちゃくちゃ良いみたいです。

苦労したこと

大量のMacのセットアップ

iOSのシミュレータを使用するには、macOS環境が必要です。

SmartQAでは、macOSを貸し出してくれるMacStadiumというサービスを利用することにしました。

借りたMacを1つ1つ設定していくのですが、それはもう大変でした。

途中からAnsibleで自動化され楽にはなったものの、それでもXcodeをインストールするのは手動じゃないとダメだったりします。(2ファクタ認証の関係で)

平成時代から続くiOSシミュレータ自体のバグ

5年前に、AppleのDeveloperフォーラムに投稿されている内容です。
Hardware keyboard not working in simulator (toggle in on)

Macのキーボードをシミュレータに接続しない設定で起動しても、接続されているバグがあるんですよね。

キーボードが接続されていると、UITextFieldにフォーカスするために、2回タップが必要だったり。

実際とは違う挙動をしてしまうんですよね。

もちろんApple信者としては、仕様として受け入れることにしました

調べた限り対策は1つしかなく、ここを2回クリックしてオンオフするしかないみたいです。

image.png

AppleScriptというものをこの時初めて知り、起動直後にConnect Hardware Keyboardを2回クリックするスクリプトを用意しました。

tell application "System Events"
  set hwKB to value of attribute "AXMenuItemMarkChar" of menu item "Connect Hardware Keyboard" of menu 1 of menu item "Keyboard" of menu 1 of menu bar item "I/O" of menu bar 1 of application process "Simulator"
  click menu item "Connect Hardware Keyboard" of menu 1 of menu item "Keyboard" of menu 1 of menu bar item "I/O" of menu bar 1 of application process "Simulator"
  click menu item "Connect Hardware Keyboard" of menu 1 of menu item "Keyboard" of menu 1 of menu bar item "I/O" of menu bar 1 of application process "Simulator"
  if ((hwKB as string) is equal to "missing value") then
    do shell script "echo 'hardware keyboard is off'"
  else
    click menu item "Connect Hardware Keyboard" of menu 1 of menu item "Keyboard" of menu 1 of menu bar item "I/O" of menu bar 1 of application process "Simulator"
  end if
end tell

iOSアプリのUI要素のXMLの情報量が極端に少ない!

Appiumでは、XCUITestdebugDescriptionを利用してWebでいうところのDOMを取得しています。

そのDOMをXML形式で取得できるのですが、classidなどの情報があるWebに比べて情報量が圧倒的に少ないです。

<XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="パスワード" name="パスワード" label="パスワード" enabled="true" x="24" y="87" width="75" height="20"/>
<XCUIElementTypeSecureTextField type="XCUIElementTypeSecureTextField" value="Abc1234" name="" label="" enabled="true" x="190" y="87" width="161" height="21"/>
<XCUIElementTypeTextField type="XCUIElementTypeTextField" value="ピッカー入力" name="" label="" enabled="true" x="24" y="131" width="327" height="22"/>
<XCUIElementTypeButton type="XCUIElementTypeButton" name="Login" label="Login" enabled="true" x="165" y="185" width="45" height="34">
  <XCUIElementTypeStaticText type="XCUIElementTypeStaticText" value="Login" name="Login" label="Login" enabled="true" x="165" y="191" width="45" height="22"/>
</XCUIElementTypeButton>

こちらがXMLの一部です。

UIButtonUITextFieldUILabelについて得られる情報は、要素の種類座標サイズがあります。

<XCUIElementTypeImage type="XCUIElementTypeImage" enabled="true" x="171" y="598" width="45" height="44"/>
<XCUIElementTypeOther type="XCUIElementTypeOther" enabled="true" x="235" y="612" width="17" height="16"/>

しかし、UIImageViewUIViewの場合は、座標サイズだけが頼りになります。

そこで、何かしらの方法を用いて要素の推定を行う必要があります。

今回は、ルールベースAIを組み合わせて推定することにしました。

ちなみに、ソースコードを変更できるのであれば、Accessibility Identifierを割り当てると、nameが固有の値になるので、追従がしやすくなります。

Appiumの謎のエラーでシミュレータが起動しなくなる

クラウドのmacOSで、いつも通り何度もシミュレータを立ち上げていたところ、Appiumでエラーが出力され、シミュレータが強制終了するようになりました。

image.png

Appiumの再インストール、Xcodeの最新版をインストールなど様々な方法を試しても治りませんでした。

ふと、macOS自体を再起動したところ正常に動き始めました。

????

憶測ですが、シミュレータを立ち上げごとにゾンビプロセスが生成されてしまってるのかもしれません。

この日を境にmacOSは、定期的に再起動される運用になりました

よかったこと

Appiumサーバーを複数立てることで並列化が簡単

テストの実行を早く終わらせるために、並列化をしようと試みました。

Appiumサーバーを複数起動することで実現することができました!

(WebDriverAgentAppiumのポートがかぶらないように気をつけてください)

FirestoreのCollection Groupが強力

SmartQAでは、セキュリティを最大限考慮した設計になっています。

project配下に情報を置くことで、セキュリティルールで縛りやすくしています。

その代わり、横断検索が大変になってしまいます。

2019年にリリースされたCollection Groupで、解決することができました。

/project/{projectID}/collectionA/{documentID}

通常、このような階層でcollectionAに対して検索する場合、それぞれのプロジェクトに対して実行しなければいけません。

しかし、CollectionGroupでは、同じコレクション名であれば横断的に検索をすることができます。

どんな階層に存在していても可能です。

インデックスを設定するだけで使用できて、非常に簡単でした。

さいごに

インフラからバックエンドフロント、更にはmacOSのシミュレータの資源割付まで、幅広い実装でエンジニア人生の集大成ともいえるプロダクトでした。

テスト実行の安定化や、機能追加に励んでいきたいと思います!

長期的には、Android対応などやることがたくさんあります。

全然人手が足らなくて、エンジニアさんやQAさんを絶賛募集しております。
少しでも興味があれば、contact@levetty.co.jp@koyatarooに連絡お待ちしております!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む