20200603のiOSに関する記事は10件です。

【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で続きを読む

App Store Connect Q & (A)

App Store Connect に関する気になることを記載する
何か分かれば、Answerとして追記する

アップロード関係

アップロードしたけどTestFlightに表示されない

Q.
アクティビティにはビルドとして表示されるが、TestFlightには表示されない
他のビルドバージョンは全て表示されているのにそのバージョンだけ抜けている

アップロードしたアプリはダウンロードできるのか

A.
多分できない

App Analysis

アップデート数を確認したいのだが

A.
App Store Connectのウェブサイトでは項目としてなかったが、iOSアプリの方からは確認できた

アップデート数の定義は?

Q.
調べても書いていない

アップデート日じゃなくて公開日はどこに書いてある?

A.
多分、アクティビティ→App Storeバージョン→配信準備完了の日付
尚、公開予約した場合は「デベロッパによるリリース待ち」となっているはず

機種ごとに各種データの集計をしたい

A.
機種を選ぶ項目がなかったため、機種ごとにはできない。
Apple TV, iPad, iPhone, iPodのいずれかの選択はできた。
また、iOSバーじょにゃあぷり

Reference

ASO(アプリストア最適化)用語集 【随時更新中】

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

マルチモジュールでIBでNamedColorを使用する方法

こちら を元に対応した内容ですが、あまり日本語で書いているところが少ないと思い書くことにしました。

XcodeのAssetでnamed colorを設定してIBで下記のように色を指定できて便利です。

alt

↑こちら の画像を参照しています

しかし、マルチモジュールの場合、IBで設定した後に以下のような警告が表示されて、Assetで定義をしていた色を変更しても色の変更が反映されないことがあります。

2018-04-26 17:30:30.009855+1200 MyApp[82987:11407859] WARNING: Unable to resolve the color named "PrimaryText" from any of the following bundles: com.myfirm.MyApp, com.myfirm.MyApp

理由としては、IB内のxmlでは下記のようにnamedColorの名前と失敗した場合の時のfallbackColorは記載されていますが、どのモジュールにnamedColorの定義がされているかが記載されていません。
そのため、他のモジュールとかに色の定義がある場合、参照できずfallbackColor(恐らくIBで設定時の色)が使用されてしまうためです。

alt

↑こちら の画像を参照しています

ならどのように防げば良いかと言うと、色のリソースはサイズがさほど大きいことはほぼないと思います。
なので、下記のように色のAssetのTargetを色を使用するモジュールそれぞれに含みfallbackColorではなくnamedColorの参照にxibやstoryboardでも成功させることで、Assetの色の定義を変更してもそれぞれのモジュールのIBで指定した色の箇所にも反映されるようになります。
(注意:色のリソースがモジュールごとに重複するのでわずかにアプリのサイズは大きくなると思います)

ColorAssetsCheck.png

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

iOSのURLスキームの脆弱性について

概要

iOSのURLスキームが意外と脆弱だから気をつけた方が良いよと言う話をします。

※違うよ、とかあれば気軽にコメント頂ければ幸いです

iOSのURLスキームについて

下記のようにURLスキームを叩くことがあると思うのですが、

hoge://〜

これってどの情報を元にどのアプリがこのURLスキームを受けるか決めているのでしょうか。

答えとしては、どのアプリが受け取ると言う情報が入っている訳ではありません。

なので

AアプリとBアプリでそれぞれ同じプロトコル名で受け取るようにしていた場合、どちらがそのURLスキームを受け取るかわかりません
(過去に確認した挙動だと、先にインストールされている方のアプリが受け取るようです)

Aアプリ

hoge://〜
を受け取るように実装

Bアプリ

hoge://〜
を受け取るように実装

奪い取りたいURLスキームのプロトコルなんて他の人わからないじゃんって思うかもしれませんが、
こちら で使用しているツールのように結構に気軽に他のアプリが受け取るURLスキームは調べることはできそうです。

Androidの場合は下記のように(アプリのパッケージ名)を指定することで起動するアプリを指定できます。

intent://〜#Intent;package=(アプリのパッケージ名);scheme=hoge;end;

SFSafariViewControllerの例外

アプリからSFSafariViewControllerを開きURLスキーム(Deeplink)で戻ってくることがあると思いますが、
その場合はSFSafariViewControllerを開いたアプリが優先的にURLスキームを受け取るようで、
他のアプリに先に奪い取られてしまうことはないようです。

まとめ

iOSでSFSafariViewController以外でURLスキームを送信・受信する際は、セキュリティに影響するような情報の受け渡しはやめよう。
iOSのURLスキームは他のアプリが受け取る可能性があることを考慮しよう。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む