20200603のSwiftに関する記事は12件です。

【プログラミング初心者】Swift基礎~if文・switch文~

はじめに

今回は条件判定のために必要な構文、if文switch文の書き方について紹介します。
条件文は多少書き方の違いはありますがSwift以外の多言語全てで使用されるプログラムの基本構文の1つです。
この投稿で書き方をしっかりと覚えておいてください。

条件文とは

プログラムでは異なる条件毎に処理を分岐させアプリを制御します。

例えば現実的な例としてマンガアプリで考えてみます。
そのアプリで購入したマンガを表示する処理を実装するとしましょう 。
その場合マンガのページを表示するにはどのような条件が考えられるでしょうか?

対象のマンガは購入済みかどうか、未購入の場合は購入するためのアプリ内マネーが足りているかどうか、通信エラーが発生したかどうか、etc.
考え出すとキリがありませんが、こういった考えられる状況全てに対して条件分岐させそれぞれ異なった処理を実装していく必要があります。

こういった場合に条件文と呼ばれるif文やswitch文を使用して制御していきます。

if文

それでは具体的な書き方を紹介していきます。
構文としては以下の通りです。

if 条件 {
    条件が真の場合の処理
} else {
    条件が偽の場合の処理
}

条件にはBool型を入れる必要があります。

ifに与えた条件がtrueの場合最初の処理ブロックに入ります。
falseとなった場合はelseの処理ブロックに入りそれぞれの処理を実行します。
またそれぞれの処理ブロックはどちらか片方しか実行されず、ifに入った場合elseは実行されません。

条件はBoolであれば変数、メソッド、式と何でも構いません。

// 変数
let boolValue = true
if boolValue {
    print("boolValue is true")
} else {
    print("boolValue is false")
}
// 式
let intValue = 1
if intValue == 1 {
    print("intValue is 1")
} else {
    print("intValue is not 1")
}
// メソッド
func boolFunction() -> Bool {
    return true
}

if boolFunction() {
    print("boolFunction() is true")
} else {
    print("boolFunction() is false")
}

elseは必須ではなくifのみでも使用できます。

if value > 0 {
    print("0より大きい")
}

またif文同士繋げることも可能です。

if value > 10 {
    print("10より大きい")
} else if value < 10 {
    print("10より小さい")
} else {
    print("それ以外")
}

条件はBoolあればいいので、論理演算子を使って複数の条件を組み合わせることができます。

if value >= 0 && value < 10 {
    print("0以上10未満")
} else {
    print("それ以外")
}

switch文

Swiftのswitchはかなり汎用性が高く、数多くの書き方があります。
今回は基本となる書き方のみを紹介します。

基本構文は以下となります。

switch  or  {
case 値1:
    値1の場合の処理
case 値2:
    値2の場合の処理
...
default:
    caseに当てはまらなかった場合の処理
}

具体的な実装は以下のようになります。

let value = 0
switch value {
case 0:
    print("0です。")
case 1:
    print("1です")
case 2:
    print("2です")
default:
    print("それ以外です")
}

このようにswitchに与えた値valueに対して判定を行ないます。
ifとの違いは条件ではなく値そのものに対する判定ということです。

当てはまるcaseから次のcaseまでの間の処理が実行されます。
それ以外の処理は実行されません。
(他言語を触った人からするとbreakがないのに次が実行されないことに違和感があるかもしれません。Swiftではbreakは不要です。)

switchに与える型は値の判定ができれば構いません。
とはいえ値の判定ができるのは数値型、文字列型、列挙型くらいで、現実的に使うのはこのあたりの型になるかとは想います。

1つのcaseに複数指定することも可能です。

let club = "野球"
switch club {
case "野球", "サッカー", "テニス":
    print("運動部です")
case "吹奏楽", "茶道":
    print("文化部です")
default:
    print("未定義の部活です")
}

また数値の場合(数値1…数値2)とすることで範囲によって条件を分岐させることも可能です。

let score = 80
switch score {
case (80...100):
    print("優")
case (70...79):
    print("良")
case (60...69):
    print("可")
case (0...59):
    print("不可")
default:
    print("未定義")
}

以上のように値に対して判定を行ない、当てはまるcaseの処理を実行するのがswitch文です。
いくつか例を紹介しましたがまだまだ使い方は色々あります。
条件判定する際は調べてみてください。

最後に

今回はプログラミングする上では欠かせない条件分岐の方法としてifswitchを紹介しました。
どちらも似たようなものですね。
実際switchif~else if~...と条件分岐を行っても同じ結果が得られます。
プログラムを管理しやすい方、ミスが起きにくい方を状況に応じて選んでいきたいところです。

今回の内容は以上です。
本記事とは別でプログラミング未経験からiOSアプリ開発が行えるようになることを目的とした記事を連載しています。
連載は以下にまとめていますのでそちらも是非もご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684

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

「this class is not key value coding-compliant for the key」のエラーが取れないとき

AppDelegateで止まってしまう、めちゃくちゃありがちなエラーです。
コンポーネントをソースファイルにドラッグして繋いだあとに、コード側だけ削除してコンポーネントの接続をバツで消さないと起こるやつですね。
AppDelegateが突然出てきたときはほぼこれなのでエラーメッセージすら読まずに「どこ繋ぎ忘れたかな」とさっと確認して即直せたのですが、今回は微妙にハマりました。

「このaction、ちゃんと繋がってるのになぜ??」とクリーンしたり再起動したり色々してもダメでした。

原因ですが、最初にうっかりoutlet接続してしまって、それが残っていました。
なんとも間抜けですが、同じ名前なので気付きにくかったです。

おそらく、どこかおかしなところに同じ名前でくっついてしまっているんだろうと、色んなところを探し回りましたが見つからず。。。

しかし、さくっと見つける方法が一つありました。
ストーリーボードのviewControllerの一番上の階層をクリックして、そこの接続を見るとなんとコードと接続されていない消すべきoutlet接続に黄色い警告マークが出ます。

まとめると「一番上の階層から黄色警告を探してみてください。おそらくそいつが消すべき接続です」という感じです。

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

【Swift】KVOでUserDefaultsの値の変更を監視する

KVOとは

KVO(Key Value Observing)の略名になります。
その名の通り、オブジェクトの値を監視する為に用いる技術です。
値の更新や削除などの変更を検知することができとても便利です!

UserDefaultsの値の変更を監視する

UserDefaultsの値をdynamicで取得する

// 今回はIntの値を扱っています
extension UserDefaults {
    @objc dynamic var translateLimit: Int {
        return integer(forKey: keyName)
    }
}

監視する処理

var observer: NSKeyValueObservation?

private func setKVO() {
    observer = UserDefaults.standard.observe(\.translateLimit, options: [.initial, .new], changeHandler: { [weak self] (defaults, change) in
        // 行いたい処理を書く
    })
}

これで監視の処理は完了!
簡単だしとても便利。

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

UnityでSwiftの自作「Static Library」を使う

環境

  • Unity 2018.4.23f1
  • Xcode 11.5
  • Swift 5
  • iOS 13.4.1(iPhone 11)

手順の概要

「Unity」から直接「Swift」のコードを呼び出すことはできないようです。。。orz
その為、下記のような形で、「Objective-C++」を経由する事で、「Swift」の関数を呼び出します。

設定手順

「Swift」の「Static Library」を作成

「Xcode」で、プロジェクトを作成

  • 「iOS」→「Framework & Library」→「Static Library」を選択して、「Next」を押下

  • 下記の項目を設定して、「Next」を押下
    ※記載以外はデフォルト値

項目
Product Name 任意
Team 任意
Organization Name 任意
Organaization Identifier 任意
Language Objective-C

  • 任意のディレクトリにプロジェクトを作成

呼び出される「Swift」ファイルの作成

  • 「プロジェクト」→「コンテキストメニュー」→「New File」を押下

  • 「iOS」→「Source」→「Swift File」を選択して、「Next」を押下

  • 任意の名前を設定して、「Create」を押下

  • 「Create Briding Header」を押下

  • 作成した「Swift」ファイルを選択して、呼び出す関数を作成

//
//  SwiftTest.swift
//  SwiftStaticLibrary
//
//  Created by Kumatta_ss on 2020/05/31.
//  Copyright © 2020 Kumatta_ss. All rights reserved.
//

import Foundation

class SwiftTest: NSObject {
    /// 呼び出しのみ
    @objc static func swiftCallTest() {
        NSLog("swiftCallTest OK!")
    }
    /// 引数のみ
    /// - Parameters:
    ///     - val1: 引数1
    @objc static func swiftCallTestArgument(val1: String) {
        NSLog("swiftCallTestArgument OK! Argument:" + val1)
    }

    /// 引数、戻り値あり
    /// - Parameters:
    ///     - val1: 引数1
    /// - Returns:戻り値
    @objc static func swiftCallTestArgumentReturn(val1: String) -> String {
        NSLog("swiftCallTestArgumentReturn OK!")
        return "Return swiftCallTestArgumentReturn Argument:" + val1;
    }
}

外部公開用の「Objective-C++」を作成する

  • 「Command + B」で、Buildする
    ※Swiftのヘッダーファイルを生成させる
  • 初期で作成されている、「Objective-C」のファイルの拡張子を、「m」を、「mm」にする
    ※「Objective-C++」に変更
  • 変更したファイルに、Swiftを呼び出す関数を作成

// Swiftの関数宣言ヘッダー(名称は、「{プロジェクト名}-Swift.h」形式)
#import <SwiftStaticLibrary-Swift.h>

// 外部に公開用の関数宣言
extern "C" {
    void CallTest();
    void CallTestArgument(const char *val1);
    const char* CallTestArgumentReturn(const char *val1);
}

// 呼び出しのみ
void CallTest() {
    NSLog(@"CallTest");
    [SwiftTest swiftCallTest];
}

// 引数のみ
void CallTestArgument(const char *val1) {
    NSLog(@"CallTestArgument");
    [SwiftTest swiftCallTestArgumentWithVal1:@(val1)];
}

// 引数、戻り値あり
const char* CallTestArgumentReturn(const char *val1) {
    NSLog(@"CallTestArgumentReturn");
    NSString *result = [SwiftTest swiftCallTestArgumentReturnWithVal1:@(val1)];

    const char *resultEncodeVal = [result cStringUsingEncoding:NSUTF8StringEncoding];
    char *returnVal = (char*)malloc(strlen(resultEncodeVal) + 1);
    strcpy(returnVal, resultEncodeVal);
    return returnVal;
}

ライブラリの生成

  • ビルドすると「Products」ディレクトリ配下に、「a」ファイルが作成される
    ファイルを選択した状態で、右のメニューの「Indentity andType」→「Full Path」の矢印を押下すると、格納ディレクトリを開きます

Unityで「Sttaic Library」を使用する

ライブラリの配置

  • 格納用ディレクトリを、下記の構成で作成
Assets
 ├─ Efitor
 ├─ Plugins
 | └─ iOS
 └─ Scripts

  • 「Assets/Plugins/iOS」に生成された、「a」ファイルを格納

  • 格納した「a」ファイル選択して、「Inspector」の下記の項目を設定して、「Apply」を押下
項目 
Select platforms for plugin -> Include Platfprms 「iOS」のみにチェック
Platform settings デフォルト

呼び出しクラスの作成

  • 呼び出し確認用のクラスを、「Assets/Scripts」に作成

using System.Runtime.InteropServices;
using UnityEngine;

public class UnityCallTest : MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern void CallTest();

    [DllImport("__Internal")]
    private static extern void CallTestArgument(string val1);

    [DllImport("__Internal")]
    private static extern string CallTestArgumentReturn(string val1);

    // Start is called before the first frame update
    void Start()
    {
        CallTest();

        CallTestArgument("1.Unityからの呼び出しだ!");

        string result = CallTestArgumentReturn("2.Unityからの呼び出しだ!");
        Debug.Log("============================ Unity Result:" + result);
    }

    // Update is called once per frame
    void Update()
    {

    }
}


  • 「Hierarchy」に、任意のGameObjectを作成して、作成したスクリプトをアタッチ

ビルド設定

  • 「Player setting」で、任意の設定をする
    ※今回は下記の赤い部分のみを変更しました

  • 「Assets/Editor」配下に、拡張エディタを作成
    そのままだと、ビルドしたXcodeプロジェクトに、毎回設定をしないといけないので、拡張エディタを作成

using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;

public class TestPostProcessBuild
{
    [PostProcessBuild]
    public static void BuildTest(BuildTarget buildTarget, string projectPath)
    {
        // iOSの場合のみ
        if (BuildTarget.iOS == buildTarget)
        {
            string projectFilePath = PBXProject.GetPBXProjectPath(projectPath);
            var proj = new PBXProject();
            proj.ReadFromFile(projectFilePath);
            string target = proj.TargetGuidByName(PBXProject.GetUnityTargetName());


            // BridgingHeaderを作成・設定
            string projSwiftBridgingFile = "Classes/Unity-iPhone-Bridging-Header.h";
            string swiftBridgingFile = Path.Combine(projectPath, projSwiftBridgingFile);
            var fs = File.Create(swiftBridgingFile);
            fs.Close();

            string swiftBridgingFileGuid = proj.AddFile(swiftBridgingFile, projSwiftBridgingFile, PBXSourceTree.Source);
            proj.AddFileToBuild(target, swiftBridgingFileGuid);
            proj.SetBuildProperty(target, "SWIFT_OBJC_BRIDGING_HEADER", projSwiftBridgingFile);

            // Xcodeのビルド設定
            proj.SetBuildProperty(target, "CLANG_ENABLE_MODULES", "YES");
            proj.AddBuildProperty(target, "LD_RUNPATH_SEARCH_PATHS", "$(inherited) @executable_path/Frameworks");
            proj.SetBuildProperty(target, "SWIFT_VERSION", "5.0");
            proj.SetBuildProperty(target, "ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES", "YES");
            proj.SetBuildProperty(target, "SWIFT_INSTALL_OBJC_HEADER", "YES");
            proj.SetBuildProperty(target, "SWIFT_PRECOMPILE_BRIDGING_HEADER", "YES");
            proj.SetBuildProperty(target, "SWIFT_OBJC_INTERFACE_HEADER_NAME", "$(PRODUCT_NAME)-Swift.h");

            proj.AddBuildProperty(target, "LIBRARY_SEARCH_PATHS", "/usr/lib/swift/");
            proj.AddFileToBuild(target, proj.AddFile("usr/lib/swift/libswiftFoundation.tbd", "Frameworks/libswiftFoundation.tbd", PBXSourceTree.Sdk));

            proj.WriteToFile(projectFilePath);
        }
    }
}

ビルドして実機で実行

  • ログに実行結果が出力されると思います

あとがき

今回、Swiftで作成されたライブラリを、Unityで呼び出す必要があり、
「Objective-C」、「Swift」を触った事ない状態から、色々試した結果です。

ビルドのプロパティについては、「Xcode」でネイティブアプリとして、
「Objective-C」と、「Objective-C + Swift」の二つを作成、解析して、必要な設定を割り出した物になります。

問題や追加の説明が欲しいなどが、あったらコメントなどで、教えていただけたら嬉しいです。

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

Embedded Frameworkでビルドエラーが出る場合

クリーンしてからの再ビルドでエラーが出る人

Embedded Frameworkを複数作成して開発していると、差分ビルドは成功するのにクリーン後のビルドで何故かFrameworkが無いとか、謎のビルドエラーなどが出る人向けです。

依存関係を定義しましょう。

ターゲット -> Build PhasesからDependenciesを展開。
依存関係にあるEmbedded Frameworkを追加しておきます。

これだけで解消します。

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

肥大化したiOSアプリを課題解決のために半年弱でリアーキテクチャした話

はじめに

2019年に約半年弱の開発期間で公開中のiOSアプリを
リアーキテクチャーした話を備忘録としてまとめました。

サービスインされているアプリを一気に作り直すケースは
ビジネス判断的にはかなり危い橋を渡っているのであまり無いとは思いますが
似たような状況で開発に臨んでいる方など、どなたかの参考になれば幸いです?

開発の背景

  • 2014年から毎年追加改修が走っているストア公開中のECアプリ案件(Objective-C)
  • 画面数で見れば中規模から大規模にカテゴライズ
  • iOSのバージョンについて古いものは毎年切り捨ててく方針
  • 開発メンバーは毎年ほぼ総入替
  • 自分は2018年頃から参加しました

抱えていた課題・技術的負債

受託案件あるあるですが以下の課題を抱えていました。

0️⃣ ピュアObjective-C
1️⃣ 可読性の低い巨大すぎるMVC
(MassiveViewController + 非同期処理通知はNSNotificationのみ, BlocksKit等の利用無し)
2️⃣ 同じ/似たような機能を再利用せずにコピペで複数箇所に実装
3️⃣ オンメモリでも全く問題ない不要なアプリ内DB(CoreData)
4️⃣ テストコード・内部設計書の無い長期運用
5️⃣ 上記の問題による運用・改修コストの増加

特に 1️⃣の影響が大きく
非同期処理の通知を受け取る箇所が広範囲に点在されていた為に
毎年入れ替わるエンジニアへの引継ぎや改修時の影響範囲の調査に
余計なコストが掛かっていました。

課題を解決するためのアプローチ

求められるアーキテクチャーの決定

当時の開発チーム全員で選定しCleanArchitectureで設計しました。

採用理由

課題解決のため責務の明確化と疎結合性の向上に加えて
長期的な運用と開発者がコロコロ替わるのに対応するため

他の候補の見送り理由

  • MVP => Datasourceを追加したとしてもPresenterが肥大化しそうだった為
  • MVVM => RxSwiftの理解が必須なため、エンジニアの入れ替えが激しい要件にそぐわない
  • VIPER => 後述のpythonを利用した旧プロジェクトから新プロジェクトへ変換が容易だった為見送り

その他

PEAKSから発行されていた iOSアプリ設計パターン入門 を輪読していたのも要因です

iOSアプリ設計パターン入門 Clean Architecture 10.4 p213 より

言い換えると、Clean Architectureはアプリが大規模で長命になるほど恩恵を受けるアーキテクチャと言えます。変化の激しい部分は容易に切り替えが可能ですし、GUI アーキテクチャではModelでまとめられていた役割をさらに分離する指針にもなります。複数人での開発や テストにおいても戸惑うことなく、整然と進められることでしょう。

開発メンバーと役割分担

??‍? : Photo, ItemList, CustomerSetting, XCTest
??‍? : Home, SideMenu, Cart, Payment, CodeConverter(Python)
??‍? : Photo, PhotoList, OCR
??‍♀️ : 途中で交代して引継ぎ Photo, CIFilter・XCUITest
Me : DI, Launch, PushNotification, DeepLink, Routing, CI

CleanArchitectureのディレクトリ構成と各ファイルの責務

iOS開発でClean Architectureを採用した際のイイ感じのディレクトリ構成とは
を参考にData, Scenes といったディレクトリに分離させました。

Data

担当したアプリは 1画面1APIのようなI/F設計にはなっていない ため画面に紐づく構成よりも
独立させて横断できる構成の方が把握が楽かなと感じました。
(※階層が若干深いので Xcodeの command + shift + j は必須でした)

DataディレクトリにはWebAPIに関連する各 Entity Entityが持つObject
Request Requestパラメータ を配置しています。

スクリーンショット 2020-06-03 14.54.37.png
スクリーンショット 2020-06-03 14.54.46.png
スクリーンショット 2020-06-03 14.54.56.png
スクリーンショット 2020-06-03 14.55.16.png

Scenes

Scenes以下に画面と紐づく名前でディレクトリを切って
各責務を担当するファイルを分けています。

スクリーンショット 2020-06-03 15.09.23.png

各責務の概要は以下です。

■Configurator 
-- ○○Assmbly.swift
Swinject用の DIグラフ解決用のファイル 
Swinject利用方法のサンプルはこちら
https://github.com/SatoshiN303/SwinjectStoryboardSample

■Domain 
--- ○○Protocols.swift
各ファイルのお互いを伝え合うProtocolをまとめて定義しておく
場所はDomain以下でなくてもよかったかも

--- ○○Gateway.swift
APIを叩いたり、Reamlからデータ取得したり 外側とやりとりする

--- ○○Usecase.swift
Gatewayから受け取ったオブジェクトをPresenter等に伝搬させる
Translatorを呼び出してEntityをModelに変換したり

--- ○○Model.swift & ○○Translator.swift
View用にEntityを加工したオブジェクトと EnityからModelへ変換する機構

※ModelとTranslatorを毎回作ってるとさすがに冗長だという意見もあり
Entityの値を加工する必要がある場合のみ作成

■Presentation
-- ○○Presenter.swift
Usecaseから受け取った値をViewへ伝搬させるなど
Presneterが肥大化するような場面では ○○DataSource.swiftを持って分散したり

-- ○○ViewController.swift
基本的にはfinalで。Scenes毎の画面遷移はSegueを使わずにVCにmakeInstanceな
static関数を生やして依存性注入する。

Objective-CのファイルをまとめてCleanArchitecture構造のSwiftファイルに変換するpythonスクリプトを利用

メンバーの提案で開発初期のスピードを早めるために
旧Xcodeプロジェクトをpythonでクロールして
CleanArchitectureの構造に定義されたSwiftファイルを
まとめて出力するスクリプトを利用しました。

Swinjectを利用したDependencyGraphの解決、
どの画面がどのWebAPIを叩いてるかのプロトコルへの紐付けなど、
あらかじめ定義できる静的な部分を生成しています。

生成例

スクリーンショット 2020-06-03 16.29.25.png

スクリーンショット 2020-06-03 17.55.00.png

私が実装したものではないので、主な処理内容だけ共有します。

前提 ディクショナリで 旧ファイル名 : 新ファイル名の変換表を保持
(1) storyBoardに関連付けられてるViewContorllerを取得
(2) 上記のViewControllerのソースコードを読み取り
(3) WebAPIを叩いてるViewController見つけたらディクショナリ生成  (vc名: [WebAPIその1, WebAPIその2]) のような
(4) 変換テーブルを用いてCleanArchitectureの形式でswiftファイルを一括生成 & 必要な定義をプロトコルに書き込んで紐付け

開発方針・規約的なもの

  • Segueは利用しない。画面遷移が発生するVCには static func makeInstance()的なものを生やして画面に必要な依存性を注入しつつDeepLink等にも対応可能にする
  • RxSwiftについてはエンジニア入れ替え時の負担を軽減するため部分的な利用に留める (Promise的に書きたい箇所やRxCocoa等など)
  • swiftLint.yml はこちら (※記述が古いかもしれません)
  • Swiftformat/CLI を利用
  • 基本はCarthage, 必要に応じでCocoaPodを利用
  • 本番、ステージング、開発環境の切り分けはxcconfigで

CIで行っていたこと

5e6f9ccc3e129dfd8a205e4e_Bitrise Logo - Eggplant Bg.png

CIはBitriseを利用していました。
受託開発での iOS アプリプロジェクト新規作成プラクティス(下編:Bitrise 編)
を参考にPullRequestをトリガーにしてDangerやSwiftLintを実行する
最低限のコードレビュー自動化やAdhoc/Releaseビルドの配布を行っています。

開発ツール・その他

開発で優先しなかったこと

  • Xcode11, iOS13対応 => タイミング的に先送り可能な時期だったので
  • 充実したテストコード => 動作が不安定だった為、全WebAPIのチェッキングのみ

作り直して解決したコト、 ポジティブなインパクト

  • ? 「どの処理がどこで何をしているか」の責務が明確になり運用・改修コストが大体0.5〜1.5人日削減
  • ?クラッシュの影響を受けていないユーザーが繁忙期計測値90%から99%へ上昇&キープ
  • ?Swinject/SwinjectStoryboard によるDIでモック可能なテスタブルで構成に
  • ?引き継ぎコストに1.5人日程度掛かっていた状態を0.5人日に短縮
  • ?注文件数 前年比130% (リアーキテクチャー以外の要因も大きいですが一応…)

残っている課題

  • embed frameworks化
  • 端末依存のコード (isIPhoneX的なもの)
  • 一部CleanArchitectureの思想から外れてしまったサイドメニューの実装等

番外編:リアーキテクチャー決定に至るまでの開発チームとしての根回し的なもの

形としては受託案件でしたので追加改修の工数を見積るタイミングで
以下を合わせて伝えておりました。(※クライアントとの関係性ありきです)

  1. 技術的負債の影響で追加改修に掛かる余計なタスク(==費用)を数値化し見積もりに含める
  2. 影響範囲の大きい要件は追加改修の影響で今後の開発コストが掛かる可能性も伝える

最初はメイン機能周辺をSwift化できれば開発チームとしては御の字だったのですが
タイミング的にビジネスを拡大して一気に攻めたいというクライアントの意向と重なり
(政治的な根回しもあり)結果的にほぼ全体をリアーキテクチャーするに至りました。

最後に

昨年の話なのでSwiftUIやiOS13については特に触れておりませんが
どなたかのお役に立てれば幸いです。
つらつらとした長文でしたが、お読み頂きありがとうございます?

参考文献・URL・スライド

非常に参考にさせて頂きました。ありがとうございます?‍♂️

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

XCTestの非同期テストで「呼び出されないこと」を検証する

非同期テスト

下記のようなサブスレッドを利用して結果を非同期に返すクラスがあるとします。

class AsyncTask {
    func execute(completion: @escaping (Bool) -> Void) {
        DispatchQueue.global().async {
            completion(false)
        }
    }
}

このクラスのテストをXCTestで書く際、以下のような実装をしてしまうと、該当のテストケースは非同期処理の完了を待たずにテストを終了させてしまいます。

class SampleTests: XCTestCase {

    var asyncTask: AsyncTask!

    override func setUpWithError() throws {
        asyncTask = AsyncTask()
    }

    func test() throws {
        asyncTask.execute { result in
            // このクロージャが呼び出される前にテストが終了してしまう
            XCTAssertTrue(result)
        }
    }

}

そのため、テストケースとしてはtrueを期待しており、AsyncTaskはfalseを返しているので、本来テストは失敗して欲しいところですが、このテストは意図とは異なり成功で終了してしまいます。

XCTestExpectation

この問題を解決するには、XCTestExpectation を利用する必要があります。
これはXCTestCase内で expectation メソッドを呼び出すことで生成でき、 XCTestCaseのwaitメソッドに受け渡すことで非同期処理が完了するまでテストの終了を待機させることができます。

XCTestCaseに非同期処理が完了したことを伝えるには XCTestExpectationの fulfill を呼び出す必要があります。
また、waitメソッドのtimeoutまでにfullfillが呼び出されなかった場合はテストが失敗することになります。

先ほどのサンプルコードをXCTestExpectationを利用した形に修正すると下記のようになります。

func test() throws {
    let expectation = self.expectation(description: "wait for async task")

    asyncTask.execute { result in
        XCTAssertTrue(result)
        // 非同期処理が完了したことを知らせる
        expectation.fulfill()
    }

    // fullfillメソッドが呼び出されるまでテストを終了させずに最大0.1秒間待機する
    self.wait(for: [expectation], timeout: 0.1)
}

これによりfullfillメソッドが呼び出されるまではテストが終了されないことを保証できるので、テストがちゃんと失敗してくれるようになります。

呼び出されないことの検証

非同期テストは基本的には上記の書き方で検証をすることができますが、たまに非同期で実行され得る処理が「呼び出されない」ことを検証しておきたいケースがあります。
このテストを実装するにはどうすればよいでしょうか。

サンプルコードを少し修正して、引数によっては完了ハンドラを呼び出さずに関数を終了させるような実装に変更してみます。

class AsyncTask {
    func execute(shouldCallCompletion: Bool, completion: @escaping () -> Void) {
        if !shouldCallCompletion {
            return
        }

        DispatchQueue.global().async(execute: completion)
    }
}

テストコードは下記のようになります。

func testShouldNotCalled() throws {
    let expectation = self.expectation(description: "wait for async task")

    asyncTask.execute(shouldCallCompletion: false) {
        expectation.fulfill()
    }

    wait(for: [expectation], timeout: 0.1)
}

非同期関数の引数にfalseを受け渡しているので、この関数は完了ハンドラを呼び出さずに終了します。
そのためfulfillメソッドが呼び出されないことでwaitメソッドのタイムアウトを超過し、テストとしては失敗します。

「テストが失敗している=呼び出されていない」という判断はできるので、期待している挙動になってはいます。
しかし、自動テストとしてこの状態をOKとすることはできません。

こういった場合に利用できるのが、XCTestExpectationの isInverted プロパティです。
このプロパティにtrueを設定しておくと、テストケースは「fullfillが呼び出されない」という挙動を期待するようになります。

func testShouldNotCalled() throws {
    let expectation = self.expectation(description: "wait for async task")
+    expectation.isInverted = true

    asyncTask.execute(shouldCallCompletion: false) {
        expectation.fulfill()
    }

    wait(for: [expectation], timeout: 0.1)
}

1行設定を追加して実行すると、テストが成功するようになります。
これによって期待している「ハンドラが呼び出されなければテストを成功させる」という挙動にすることができました:tada:

注意点

この isInverted プロパティは「timeoutを超過するまでにfullfillが呼ばれなければ成功」という形でテストケースを扱うようになるため、timeoutに大きな値を設定していた場合、テストの実行時間が増加してしまう可能性があります。

そのため、必要最小限の値を設定しておくことをお勧めします。

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

Swift UIViewControllerにtableViewを表示する(標準で用意されているセル採用パターン)

ListViewController.swift

import UIKit

class ListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    let dataList: Array = ["01","02"]
    @IBOutlet weak var tableView: UITableView!
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self;
        tableView.dataSource = self;
        // Cell名の登録をおこなう.
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        // Do any additional setup after loading the view.
    }

    //追加③ セルの個数を指定するデリゲートメソッド(必須)
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataList.count
    }

    //追加④ セルに値を設定するデータソースメソッド(必須)
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // セルを取得する
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        // セルに表示する値を設定する
        cell.textLabel!.text = dataList[indexPath.row]
        return cell
    }
}

ListViewController.xib

tableViewを配置し、ListViewController.swiftとOutlet接続を行ってください。

ListViewController.swift@IBOutlet weak var tableView: UITableView!

ListViewController.swiftでコードで生成行う場合は.xibファイル自体不要です。

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

XcodeGenを用いて中規模プロジェクトをリリースした話

はじめに

久しぶりの投稿。
題名の通り、期間は半年、エンジニアは二人という開発体制のプロジェクトにXcodeGenを導入し、先日プロジェクトをリリースしましたので、その知見を共有します。

XcodeGenとは?

iOSアプリをチーム開発のついて回る問題、project.pbxprojファイルがコンフリクトしまくる問題があります。その問題解決できるツールがXcodeGenです。
最近取り入れてるプロジェクトは増えてきてるとはいえ、まだまだ発展途上のツールでガンガンアプデされており、機能がもりもりです。
XcodeGenは主に以下をやってくれます。(*1)

XcodeGenがやってくれること

  • コマンド一発で.xcodeprojproject.ymlの設定を元に生成。
    • ライブラリ依存、フレームワークも管理
    • Build Configurationも管理
    • Development Team、Provisioning Profileも管理
    • Embedded Frameworkも管理
    • etc...
  • ファイルソートもしてくれる

要は.xcodeprojファイルをproject.ymlに置き換えてるだけですね。
そうするとコンフリクト問題になっていたディレクトリ、ファイル構成は実際のディレクトリ構成から作成するので、コンフリクトはほぼなくなります。
後は、.xcodeprojを丸ごと.gitignore指定してコマンドを開発ルールに取り入れれば、OKです。

https://github.com/yonaskolb/XcodeGen/blob/master/Docs/ProjectSpec.md が一番参考になります。

完成設定ファイル

MintでXcodeGenを導入。
リリース後完成した project.yml は以下です。(プロジェクト名や長いライブラリ依存記述などは書き換えてますので、ご容赦を)
個別設定ファイルは.xcconfigに切り分けてたりしました。

project.yml
name: test-xcodegen
# BuildConfiguration定義
configs:
  Debug: debug
  Stg: debug
  Release: release
# 別途読み込みxcconfigファイル
configFiles:
  Debug: configs/Debug.xcconfig
  Stg: configs/Stg.xcconfig
  Release: configs/Release.xcconfig
# オプション
options:
  developmentLanguage: ja
# テンプレ設定
settingGroups:
  testSettings:
    SWIFT_OBJC_BRIDGING_HEADER: ${PRODUCT_NAME}/Applications/test-Bridging-Header.h
    CODE_SIGN_STYLE: Manual
    SWIFT_VERSION: 5.0
    TARGETED_DEVICE_FAMILY: "1,2"
    INFOPLIST_FILE: test/Resources/Info.plist
    CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: YES
    OTHER_LINKER_FLAGS: $(inherited) -ObjC
    CODE_SIGN_ENTITLEMENTS: test/Resources/test_development.entitlements
    DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
  testFrameworkSettings:
    CODE_SIGN_STYLE: Automatic
    LD_RUNPATH_SEARCH_PATHS: ${inherited} @executable_path/Frameworks @loader_path/Frameworks
    PRODUCT_BUNDLE_IDENTIFIER: test-framework.${PRODUCT_NAME}
targets:
  # メインプロジェクト
  test:
    type: application
    platform: iOS
    scheme: {}
    deploymentTarget: "11.0"
    sources:
      - test
      - path: test/Resources/Generated/Assets-Constants.swift
        optional: true
        type: file
      - path: test/Resources/Generated/Colors-Constants.swift
        optional: true
        type: file
      - path: test/Resources/Generated/L10n-Constants.swift
        optional: true
        type: file
    # メイン設定
    settings:
      groups: [testSettings]
      configs:
        Debug:
          ODE_SIGN_IDENTITY: Apple Development
          DEVELOPMENT_TEAM: hogehoge
          PROVISIONING_PROFILE_SPECIFIER: test.debug
        Stg:
          CODE_SIGN_IDENTITY: Apple Distribution
          DEVELOPMENT_TEAM: hogehoge
          PROVISIONING_PROFILE_SPECIFIER: test.stg
        Release:
          CODE_SIGN_IDENTITY: iPhone Distribution
          DEVELOPMENT_TEAM: hogehoge
          PROVISIONING_PROFILE_SPECIFIER: test.release
          CODE_SIGN_ENTITLEMENTS: test/Resources/test_production.entitlements
    # 依存ライブラリ、フレームワーク
    dependencies:
      - target: TestFramework
      - framework: SDK/Ad/test.framework
        embed: false
      - carthage: NavigationNotice
      - carthage: Nuke
      - carthage: Reusable
      - carthage: RxCocoa
      - carthage: RxRelay
      - carthage: RxSwift
      - carthage: RxSwiftExt
      - carthage: RxWebKit
      - carthage: SVProgressHUD
      - carthage: TagListView
      - carthage: TransitionableTab
    # 追加Build Phases
    preBuildScripts:
      - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/app-swiftgen.yml
        name: Generate resources with SwiftGen
        outputFiles:
          - ${PRODUCT_NAME}/Resources/Generated/Assets-Constants.swift
          - ${PRODUCT_NAME}/Resources/Generated/Colors-Constants.swift
          - ${PRODUCT_NAME}/Resources/Generated/L10n-Constants.swift
      - script: |
                mint run mono0926/LicensePlist license-plist --output-path ${PRODUCT_NAME}/Resources/Settings.bundle
        name: Run license-plist
      - script: |
                cp "${PROJECT_DIR}/${PROJECT_NAME}/Resources/Firebase/GoogleService-Info_${CONFIGURATION}.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
        name: Run Firebase
    postBuildScripts:
      - script: mint run SwiftLint swiftlint
        name: Run SwiftLint
      - script: "\"${PODS_ROOT}/FirebaseCrashlytics/run\""
        name: Run Crashlytics
        inputFiles:
          - ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
          - $(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
  testTests:
    # テスト設定は省略
  testUITests:
    # テスト設定は省略
  # Embedded Framework
  TestFramework:
    type: framework
    platform: iOS
    scheme: {}
    deploymentTarget: "11.0"
    sources:
      - Datasource
      - path: "TestFramework/Resources/Generated/L10n-Constants.swift"
        optional: true
        type: file
    settings:
      groups: [testFrameworkSettings]
    dependencies:
      - carthage: APIKit
      - carthage: CryptoSwift
      - carthage: Realm
      - carthage: RealmSwift
      - carthage: SwiftProtobuf
    preBuildScripts:
      - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/framework-swiftgen.yml
        name: Generate resources with SwiftGen
        outputFiles:
          - ${PRODUCT_NAME}/Resources/Generated/L10n-Constants.swift
  TestFrameworkTests:
    # テスト設定は省略
env.xcconfig
// Debug.xcconfig
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG
GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 $(inherited)
GCC_OPTIMIZATION_LEVEL = 0
ONLY_ACTIVE_ARCH = YES
ENABLE_TESTABILITY = YES
GCC_DYNAMIC_NO_PIC = NO
MTL_ENABLE_DEBUG_INFO = YES
SWIFT_OPTIMIZATION_LEVEL = -Onone
OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -debug-time-function-bodies
DISPLAY_NAME_PREFIX = debug-
PRODUCT_BUNDLE_IDENTIFIER = test.debug
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-debug
// Stg.xcconfig
SWIFT_ACTIVE_COMPILATION_CONDITIONS = STG
GCC_PREPROCESSOR_DEFINITIONS = STG=1 $(inherited)
GCC_OPTIMIZATION_LEVEL = 0
ONLY_ACTIVE_ARCH = YES
ENABLE_TESTABILITY = YES
GCC_DYNAMIC_NO_PIC = NO
MTL_ENABLE_DEBUG_INFO = YES
SWIFT_OPTIMIZATION_LEVEL = -Onone
OTHER_SWIFT_FLAGS = $(inherited) -Xfrontend -debug-time-function-bodies
DISPLAY_NAME_PREFIX = stg-
PRODUCT_BUNDLE_IDENTIFIER = test.stg
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-stg
// Release.xcconfig
ENABLE_NS_ASSERTIONS = NO
VALIDATE_PRODUCT = YES
MTL_ENABLE_DEBUG_INFO = NO
SWIFT_OPTIMIZATION_LEVEL = -Owholemodule
PRODUCT_BUNDLE_IDENTIFIER = test.release
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon

知見

色々困ったことや工夫したことを書いていきます。とはいえ、まだまだ新しい記述なども増えていってますので、ProjectSpecを見ながら参考にすると良いです。

Build Configuration

project.yml
# BuildConfiguration定義
configs:
  Debug: debug
  Stg: debug
  Release: release

定義は簡単に書けます。
後は、定義名をキーにして、設定を分けて書いたりできます。

個別のプロジェクト設定

project.yml
targets:
  # メインプロジェクト
  test:
    # メイン設定
    settings:
      configs:
        Debug:
          ODE_SIGN_IDENTITY: Apple Development
          DEVELOPMENT_TEAM: hogehoge
          PROVISIONING_PROFILE_SPECIFIER: test.debug

個別の設定は上記のように書くことができます。

project.yml
# 別途読み込みxcconfigファイル
configFiles:
  Debug: configs/Debug.xcconfig
  Stg: configs/Stg.xcconfig
  Release: configs/Release.xcconfig

ですが、個別の設定をproject.ymlに埋め込むと煩雑になってしまいました。
そこで configs/{env}.xcconfig の記述を逃して、管理するようにしました。

共通のプロジェクト設定

project.yml
# テンプレ設定
settingGroups:
  testSettings:
    # 設定
  testFrameworkSettings:
    # 設定
targets:
  # メインプロジェクト
  test:
    settings:
      groups: [testSettings]
  # EmbbededFramework
  TestFramework:
    settings:
      groups: [testSettings]

settingGroups で定義すれば、ここのtargetで用いれるようになるので便利です。
なので、基本的には共通の設定はsettingGroupsで、BuildConfigurationごとの個別設定は.xcconfigに分けるようにしました。

ソースコード

project.yml
targets:
  # メインプロジェクト
  test:
    sources:
      - test # 参照ディレクトリ

ソースコードはディレクトリ指定さえしておけば、再起的にCompileSourceとして読み込んでくれます。

ビルド時生成ファイルの参照

project.yml
targets:
  # メインプロジェクト
  test:
    sources:
      # ビルド時生成ファイルの参照記述
      - path: test/Resources/Generated/Assets-Constants.swift
        optional: true
        type: file
      # 省略
    preBuildScripts:
      - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/app-swiftgen.yml
        name: Generate resources with SwiftGen
        outputFiles:
          - ${PRODUCT_NAME}/Resources/Generated/Assets-Constants.swift
          # 省略

SwiftGenR.swift などのビルド時生成ファイルが存在する場合は、参照先を上記のように加えなければなりません。
さらにBuild Phasesの追加スクリプト、preBuildScripts.outputFilesでそれぞれ記述する必要があります。

依存ライブラリ、フレームワーク

project.yml
targets:
  # メインプロジェクト
  test:
    # 依存ライブラリ、フレームワーク
    dependencies:
      - target: TestFramework # EmbbededFramework
      - framework: SDK/Ad/test.framework # 手動読み込みフレームワーク
        embed: false
      - carthage: NavigationNotice # Carthage
      - carthage: Nuke

EmbbededFramework、Carthageは簡単に記入できます。
違う方法もあるかもしれませんが、手動読み込みフレームワークはプロジェクトディレクトリ外+Path指定してあげる必要がありました。(*1)
Swift Packageもdependencies.package の記述のみで行けるので、試してみたいですね。

CococaPods

当初はCocoaPodsは導入せず、Carthageだけで行こうと思いましたが、広告系の導入はCocoaPodsが必要となり、導入することになりました。ですが、XcodeGenでは、CocoaPodsの依存解決が対応してませんでした。(*1)
そこで以下のようなMakeコマンドを追加してXcodeGenした後にpod installをするルールに切り替えました。

Makefile
xcodegen:
    mint run XcodeGen xcodegen
    bundler exec pod install

xcconfigファイル、Pod用のビルドスクリプトを記述すれば、解決できるかもですが、Podの方に依存した方が安全と判断したため、上記のやり方にしました。

Build Phases

preBuildScripts

project.yml
targets:
  # メインプロジェクト
  test:
    preBuildScripts:
      - script: ${PODS_ROOT}/SwiftGen/bin/swiftgen config run --config swiftgen/app-swiftgen.yml
        name: Generate resources with SwiftGen
        outputFiles:
          - ${PRODUCT_NAME}/Resources/Generated/Assets-Constants.swift
          - ${PRODUCT_NAME}/Resources/Generated/Colors-Constants.swift
          - ${PRODUCT_NAME}/Resources/Generated/L10n-Constants.swift
      - script: |
                mint run mono0926/LicensePlist license-plist --output-path ${PRODUCT_NAME}/Resources/Settings.bundle
        name: Run license-plist
      - script: |
                cp "${PROJECT_DIR}/${PROJECT_NAME}/Resources/Firebase/GoogleService-Info_${CONFIGURATION}.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
        name: Run Firebase

ビルド前に走らせるスクリプトです。
LicensePlist、SwiftGen、GoogleService-Info.plistの環境別読み込みなどしてます。

postBuildScripts

project.yml
targets:
  # メインプロジェクト
  test:
    postBuildScripts:
      - script: mint run SwiftLint swiftlint
        name: Run SwiftLint
      - script: "\"${PODS_ROOT}/FirebaseCrashlytics/run\""
        name: Run Crashlytics
        inputFiles:
          - ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
          - $(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)

ビルド後に走らせるスクリプトです。
SwiftLint、FirebaseCrashlyticsなどを走らせてます。

CI

今回のプロジェクトはBitriseを採用しており、環境ごとにdeploygateアップロード、AppStoreConnectアップロードをしています。

スクリーンショット 2020-06-02 15.02.27.png

画像の通り、XcodeGenを走らせるコマンドstepを導入するだけで簡単です。
先述でありました、Cocoapodsのインストール前に行うのを忘れずに。

結論

導入コスト、学習コストに関しては基本的にProjectSpecを参考にしていれば、大体は大丈夫でした。
ですが、開発途中でどうしても細かいプロジェクト設定を適用する状況がありますのでその際に一旦Xcodeより設定してみてビルド。ビルド確認してOKそうならproject.ymlに落とし込む作業が多々発生し、なかなか骨が折れました。(もっと良い方法があるのかも)

結局そこまでコンフリクトが問題にならないプロジェクトでした。
何よりドキュメントが少ない、導入実績も現時点(*1)でそこまで多くないので、先述の修正コストの方がでかかったです。今回のような規模のプロジェクトで初導入検討ならいらないかなと思いましたw

脚注

(*1) 2020年6月現在では

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

追加要件:FCMにて画像付きPush通知を送る iOS10以上

前置き

  • FCMコンソールからのPush通知が実装済みからの追加要件という内容です。
  • 今回の記事では送信方法としてPostmanを使用しております。
  • なるべく細かく書くつもりですが、後半になるにつれ適当になる可能性があります。

概要

iOS10から追加された Notification Service Extension を利用する。
これは「送信されたペイロード」 -> 「端末に表示」 までの間の部分をあれこれ出来る。
そのため表示される前に画像を読み込んで、それを表示させるといった流れを行える。

目次

  • Notification Service Extension追加
  • Notification Service Extension用のProvisioningファイルを用意
  • Notification Service Extension編集
  • 送信

Notification Service Extension追加

  1. XcodeメニューバーのFile -> New -> Target
  2. Notification Service Extension を選択 -> Next
    スクリーンショット 2020-06-03 11.19.05.png

  3. Product Nameを入力(ex: NotificationService) -> Finish

    「MainProjectのBundleIdentifier.上記ProductName」という形式のBundleIdentifierとなる。
    言わずともこれは後述するProvisioningファイルのBundle IDとなる。

  4. ダイアログが出たらActivateを選択

  5. NotificationServiceExtensionディレクトリ配下にNotificationService.swiftInfo.plistがあるものが追加されていればOK
    NotificationService.swiftには下記2つのメソッドが既存で作成されている。

NotificationService
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    ///省略
}
override func serviceExtensionTimeWillExpire() {
    ///省略
}

Notification Service Extension用のProvisioningファイルを用意

MainAppと異なるBundleIdentifierをもつTargetが追加されたため、
これに対してProvisioningファイルを用意する必要がある。

  1. AppleDeveloper SignIn
  2. Certificates, Identifiers & Profiles -> Identifiers -> 「+」押下
    スクリーンショット 2020-06-03 10.59.43.png

  3. App IDs 選択 -> Continue

  4. 各項目入力 -> 作成

    Description: NotificationServiceExtension
    Bundle ID: 「MainProjectのBundleIdentifier.先程のProductName」
    ※コピーするとエラーが出ることがあるようで、その場合は手動入力をする必要がある。

  5. Certificates, Identifiers & Profiles -> Profiles -> 「+」押下

  6. App Store -> 先ほど作成したApp IDs選択 -> メインと同じCertificateファイルで作成

  7. 作成すると既存Provisioningファイルが無効になるため Edit -> Save をし更新する。

  8. xcodeに戻る。

  9. XcodeメニューバーのXcode -> Preferences -> Download Manual Profiles より先ほど作成したProvisioningファイルをDL

  10. 各TARGETSに適切なProvisioningProfileを選択
    スクリーンショット 2020-06-03 12.11.27.png

  11. MainAppのSigning & CapabilitiesにBackground Modesを追加
    スクリーンショット 2020-06-03 12.13.42.png

Notification Service Extension編集

NotificationService.swift
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {

    self.contentHandler = contentHandler
    bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

    self.realmWrite(request: request, bestAttemptContent: bestAttemptContent)

    if let imageUrl = request.content.userInfo["image_url"] as? String {
        let session = URLSession(configuration: URLSessionConfiguration.default)
        let task = session.dataTask(with: URL(string: imageUrl)!, completionHandler: {[weak self](data, response, error) in
            do {
                if let writePath = NSURL(fileURLWithPath:NSTemporaryDirectory())
                    .appendingPathComponent("tmp.jpg") {
                    try data?.write(to: writePath)
                    let identifier = "hogehoge"

                    if let bestAttemptContent = self?.bestAttemptContent {
                        let attachment = try UNNotificationAttachment(identifier: identifier, url: writePath, options: nil)
                        bestAttemptContent.attachments = [attachment]
                        contentHandler(bestAttemptContent)
                    }
                } else {
                    if let bestAttemptContent = self?.bestAttemptContent {
                        contentHandler(bestAttemptContent)
                    }
                }
            } catch let error as NSError {
                print(error.localizedDescription)

                if let bestAttemptContent = self?.bestAttemptContent {
                    contentHandler(bestAttemptContent)
                }
            }
        })
        task.resume()
    } else {
        if let bestAttemptContent = self.bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}

override func serviceExtensionTimeWillExpire() {
    if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
        // 実行時間をすぎた場合の救済
        contentHandler(bestAttemptContent)
    }
}

送信

送り先

POST: https://fcm.googleapis.com/fcm/send

Header

Header
Content-Type:application/json
Authorization:key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  

スクリーンショット 2020-06-03 12.24.22.png

  1. Firebase
  2. 歯車アイコン -> プロジェクト設定 -> CloudMessaging
    スクリーンショット 2020-06-03 12.27.11.png

  3. サーバーキーのトークンをコピー
    スクリーンショット 2020-06-03 12.31.17.png

  4. 「xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx」をコピーしたトークンに変更

Body

Body
{
    "data" : {
        "image_url":"https://hogehoge.jp/fugafuga.png",
        "type" : "1"
    },
    "notification" : {
        "title": "タイトル",
        "body": "ボディ"
    },
    "mutable_content": true,
    "content_available": true,
    "to" : "token or topic"
}

スクリーンショット 2020-06-03 12.24.55.png

Key:toに指定するもの

  1. 少しAppDelegateに追加
AppDelegate.swift
extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
        print("fcmToken:\(fcmToken)")
        Messaging.messaging().subscribe(toTopic: "/topics/global") { error in
            if let error = error {
                print("TopicErrorRegistError: \(error)")
            }
        }
    }
}

この状態でビルドし、コンソールに表示されたものを利用。

  • デバイス単体に送りたい場合はfcmToken:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx部分
  • topic購読している全デバイスに送りたい場合は/topics/global

toTopicはお好きなもので。

いざ!送信!!
届かなかった方はコメントください。

追記

Debug

NotificationServiceがきちんと走っている??
と気になりDebugで止めたい場合はMainAppをビルドし一旦メインをデバイスに流した後、NotificationServiceを指定してビルドすることで可能

スクリーンショット 2020-06-03 12.52.35.png

脱線

今回の内容と少し違いますが
AppGroupsを利用することでMainとPushに共通データを持つことが可能。
例えば通知を見逃したユーザのために、通知履歴などをアプリに実装。

~流れ~
1. 通知を送る
2. NotificationService.swiftにてRealm等に保存処理(SharingPath)
3. MainAppにてSharingPathを元に通知Appにて保存したデータを共有Realmから読み取る
4. データを元に履歴List画面作成

jsonのdataに任意の値(この記事でいうtype)を追加するなどすれば、履歴リストからタップし、画面遷移なども行えますね!

需要がありそうなら細かく書きますが、、
こんなこともできますよーという話でした。

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

SwiftUI使用時の `precondition failure: invalid input index: 2` のクラッシュ修正する

SwiftUIとCombineを使用している時に、ObservableObjectの値が素早く変更される時(自分の場合だと初期値が流れる→UserDefaultsを読み込んで値が流れる)に、UIコンポーネントの表示切り替えを if文で行っていると precondition failure: invalid input index: 2 でクラッシュするという問題がありました。

解決方法

現状 if で分岐してクラッシュを回避するのは難しそうだったので、frameのheightを指定して非表示にするようにしました。(VStackの場合)もし、HStackを使用しているならwidthを指定すればいいと思います。
さらに、Imageなどのコンポーネント自体がサイズを持っている場合は親のサイズを変えても見えてしまう場合があるので、Opacityを0にします。

問題箇所の抜粋

    @State private var dataModel = DataModel()

    var body: some View {
        NavigationView {
            VStack {
                if dataModel.componentHidden {
                    SomeComponent()
                }

                SomeComponent()
            }
        }
        .onReceive(adapter.store.computed.dataModel) { (dataModel) in
            self.dataModel = dataModel
        }
    }

修正後

    @State private var dataModel = SupportedCardListTabDataModel()

    var body: some View {
        NavigationView {
            VStack {
                SomeComponent()
                .frame(height: dataModel.isAdvertiseHidden ? .zero : nil)
                .opacity(dataModel.isAdvertiseHidden ? .zero : 1)

                SomeComponent()
            }
        }
        .onReceive(adapter.store.computed.dataModel) { (dataModel) in
            self.dataModel = dataModel
        }
    }

参考資料

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

【プログラミング初心者】Swift基礎~演算子~

はじめに

プログラミングで何かを処理する場合は計算を行ないます。
今回は計算を行うための方法を紹介します。

演算子

計算をするとき、数学と同様に式が用いられます。
この式の中で+などの記号を使って計算しますが、この記号を演算子(Operator)と呼びます。
一方で演算子に計算される値を被演算子(Operand)と呼びます。

例えばlet val = 1 + 2の場合、=+が演算子で1、2が被演算子です。
オペランドの型は全て同じ必要があり、let val = 1 + 0.9などは許容されません。

演算子には色々種類があるので代表的なものを紹介していきます。

代入演算子

=

変数を値を格納するときに使用します。

let val = 1

数学で使う=は等価を意味しますがプログラム上では代入を意味します。

算術演算子

値の数値計算を行うための演算子です。
計算された値の型はオペランドと同じ型になります。

+

左右のオペランドを加算した結果を返します。

let val = 1 + 2  // 3 

+の場合は文字列など他の型にも対応できる場合があります。

let val = "文字列を" + "結合"  // "文字列を結合"

-

左のオペランドから右のオペランドを減算した値を返します。

let val = 1 - 2  // -1 

*

左右のオペランドを乗算した値を返します。

let val = 2 * 3  // 6 

/

左のオペランドから右のオペランドを除算します。

let val = 4 / 2  // 2

注意が必要なのは先述したように計算された値の型はオペランドと同じ型になります。
つまりオペランドがInt型の場合は結果もInt型となり整数となります。
割り切れない場合は少数部が切り捨てられます。

let val = 5 / 2      // 2
let val = 5.0 / 2.0  // 2.5

%

左のオペランドから右のオペランドを除算した余りを返します。

let val = 5 / 2  // 1

+=

変数に右辺の値を加算します。

var val = 1
val += 9  // val: 10
// val = val + 9 と同義

-=

変数から右辺の値を減算します。

var val = 10
val -= 8  // val: 2
// val = val - 8 と同義

比較演算子

比較演算子は等号、不等号など、左右のオペランドを比較した結果を返します。
結果は真偽値を表すBool型となります。
Booltruefalse2つの値しか取らず、それぞれ真、偽を表します。

==

左右のオペランドが等価か比較します。

let val1 = 1 == 1 // true
let val2 = 1 == 2 // false
let val3 = "文字列" == "文字列" // true

!=

左右のオペランドが等価ではないかを比較します。

let val1 = 1 != 1 // false
let val2 = 1 != 2 // true
let val3 = "文字列" != "文字列" // false

>

大なりを表し、左右のオペランドを比較した結果を返します。

let val1 = 1 > 0 // true
let val2 = 1 > 2 // false
let val3 = 1 > 1 // false

<

小なりを表し、左右のオペランドを比較した結果を返します。

let val1 = 1 < 0 // false
let val2 = 1 < 2 // true
let val3 = 1 < 1 // false

>=

大なりイコールを表し、左右のオペランドを比較した結果を返します。

let val1 = 1 >= 0 // true
let val2 = 1 >= 2 // false
let val3 = 1 >= 1 // true

<=

小なりイコールを表し、左右のオペランドを比較した結果を返します。

let val1 = 1 <= 0 // false
let val2 = 1 <= 2 // true
let val3 = 1 <= 1 // true

論理演算子

ANDやORなどの論理演算を行ないます。
オペランドはBoolで結果もBoolを返します。

!

否定(NOT)を意味します。

let value1 = true
let result1 = !value // false

let value2 = false
let result2 = !value // true

NOT.png

Aの値 演算結果
true false
false true

&&

論理積(AND)を意味します。つまり「AかつB」です。

let leftValue1 = true
let rightValue1 = true
let result1 = leftValue1 && rightValue1 // true

let leftValue2 = true
let rightValue2 = false
let result2 = leftValue2 && rightValue2 // false

AND.png

Aの値 Bの値 演算結果
true true true
true false false
false true false
false false false

||

論理和(OR)を意味します。つまり「AまたはB」です。

let leftValue1 = true
let rightValue1 = false
let result1 = leftValue1 || rightValue1 // true

let leftValue2 = false
let rightValue2 = false
let result2 = leftValue2 || rightValue2 // false

OR.png

Aの値 Bの値 演算結果
true true true
true false true
false true true
false false false

組み合わせる

演算子を1つの式の中に複数入れることも可能です。
算術演算子の場合は数学と同じで乗算、除算から計算されます。
また()で括ると括った部分から計算されます。

let val1 = 1 + 2 - 3 * 4 / 5    // 1
let val2 = (1 + 2 - 3) * 4 / 5  // 0

論理演算子も同様に組み合わせることができます。
たとえば「1 < x < 10」を判定する場合以下のメソッドで判定することができます。

func validate(x: Int) -> Bool {
     return x > 1 && x < 10
}

let result1 = validate(x: 5)   // true
let result2 = validate(x: 15)  // false

プログラムではこのように様々な演算子を組み合わせて演算していき、処理を処理を制御します。

最後に

今回は代表的な演算子について紹介しました。
紹介した演算子はプログラミング言語において基本的なもので、Swift以外の多言語でもほぼ同様の書き方ができます。
このあたりはかなりの頻度で使うので覚えていきましょう。

今回の内容は以上です。
本記事とは別でプログラミング未経験からiOSアプリ開発が行えるようになることを目的とした記事を連載しています。
連載は以下にまとめていますのでそちらも是非もご覧ください。
アジェンダ:https://qiita.com/euJcIKfcqwnzDui/items/0b480e96166e88945684

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