20191209のSwiftに関する記事は19件です。

Xcodeで簡単なアプリケーションを作成する

はじめまして、Reavensです

iPhoneのアプリケーションを開発したくなったので書いてみました
XcodeもSwiftも初めての方向けです
参考程度になればと思います

1.環境構築

まずMacにxcodeをダウンロードしていきましょう

Apple storeでXcodeを検索してダウンロードします
※iTunesアカウントが必要です。持っていない方は先につくりましょう
スクリーンショット 2019-12-09 23.20.20.png

2.プロジェクトをつくります

新規プロジェクト作成

開発に必要なプロジェクトを作成します
Xcodeを起動させます
スクリーンショット 2019-12-09 22.36.00.png
'Create a new Xcode project'をクリック
スクリーンショット 2019-12-09 22.39.21.png
'Single new APP'をクリック
スクリーンショット 2019-12-09 22.42.31.png
'Product Name','Organization Name','Organization Identifier'を入力します
'User Interface'をStoryboardにします
'Next'をクリック
スクリーンショット 2019-12-09 22.47.03.png
プロジェクトを保存する場所を選び、'create'をクリック

3.地図アプリを作ってみよう

MapKit.frameworkを使ってみる

誰でもすぐに使えるframeworkを使って地図アプリをつくリましょう
スクリーンショット 2019-12-09 22.48.50.png
'Frameworks'に'Mapkit.framework'を追加します

Main.storyboardをクリックして右上にある+から'Map kit view'を検索して、エディタエリアにドラッグ&ドロップします
サイズを四隅に合わせます
スクリーンショット 2019-12-09 22.56.28.png
左上の▶️ボタンでシミュレータを起動します
スクリーンショット 2019-12-09 23.05.46.png
シミュレータが起動したら成功です!
スクリーンショット 2019-12-09 22.10.02.png

4.地図の種類を変更してみよう

Map viewのtypeから地図の種類を変更してみます
スクリーンショット 2019-12-09 22.28.57.png

Type: Standard
地図を表示します
スクリーンショット 2019-12-09 22.10.02.png

Type: Satelite
衛星写真を表示します
スクリーンショット 2019-12-09 22.19.28.png

Type: Hybrid
地図と衛星を合わせて表示します
スクリーンショット 2019-12-09 22.26.20.png

4.まとめ

今回は環境構築からframeworkの使い方までを一通りやりました
Xcodeはコードを書いてすぐに実行や確認ができるため、開発が初めての方にとっても使いやすいと感じました
次回はもう少し踏み込んだこともやってみたいです!

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

ARKitのはじめかた その3「オブジェクトを配置する(ARKit2版)」

はじめに

こちらの続きです▶︎ ARKitのはじめかた その2「オブジェクトを配置する(ARKit1版)」

こんにちは!
ARKitのまとめ記事 にて書いた実装方法について
次はARKit2時代から使われるようになった「オブジェクト配置の方法」を書きます。
前の記事の方法より汎用性が高いのでなるべくこちらの方法を推奨します。

ゴール

タッチした場所に飛行機が出てきます。
iOS のファイル (2).gif

前提

ARKitのはじめかた その1「5分で出来るARアプリ」で作成した環境をベースとします。
※依存関係は無いのでオブジェクトを置き換えれば他でも使えると思います。

コード

ViewController.swift
import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate {
   @IBOutlet var sceneView: ARSCNView!

     override func viewDidLoad() {
       super.viewDidLoad()

       sceneView.delegate = self
       sceneView.scene = SCNScene()
   }

   override func viewWillAppear(_ animated: Bool) {
       super.viewWillAppear(animated)

       let configuration = ARWorldTrackingConfiguration()
       sceneView.session.run(configuration)

       let gesture = UITapGestureRecognizer(target: self, action:#selector(onTap))
       self.sceneView.addGestureRecognizer(gesture)
   }

    @objc func onTap(sender: UITapGestureRecognizer) {

        let pos = sender.location(in: sceneView)
        let results = sceneView.hitTest(pos, types: .featurePoint)
        if !results.isEmpty {
            let anchor = ARAnchor(name:"shipAnchor",
                                  transform:results.first!.worldTransform)
            sceneView.session.add(anchor: anchor)
        }
    }


   // MARK: - ARSCNViewDelegate

    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        if anchor.name == "shipAnchor" {
            guard let scene = SCNScene(named: "ship.scn",inDirectory: "art.scnassets") else { return }
            let shipNode = (scene.rootNode.childNode(withName: "ship", recursively: false))!
               node.addChildNode(shipNode)
        }
   }

}

解説

1.空のシーンを読み込んで、iPhoneの画面に反映させます。※前回と一緒です

    override func viewDidLoad() {
        super.viewDidLoad()

        sceneView.delegate = self
        sceneView.scene = SCNScene()
        //画面に、何もオブジェクトが無い空のシーン(映像空間)を適用させます。

        let gesture = UITapGestureRecognizer(target: self, action:#selector(onTap))
        self.sceneView.addGestureRecognizer(gesture)
        //タップジェスチャーを追加
    }

2.現実とカメラ越しの映像を連携させます。(AR機能をONにする)※前回と一緒です

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let configuration = ARWorldTrackingConfiguration()
        //AR環境の設定ファイル作ります。これによりAR機能が使えるようになります。

        sceneView.session.run(configuration)
        //画面の設定にARの設定を反映させます。
    }

3.タッチした場所にARAnchorを配置します。

    @objc func onTap(sender: UITapGestureRecognizer) {
    //画面にタッチした時に発動します。タッチした場所を、senderという変数に入れます。

        let pos = sender.location(in: sceneView)
        //一番始めにタッチしたsceneView上の場所をposとします。

        let results = sceneView.hitTest(pos, types: .featurePoint)
        //posの延長線上にある特徴点を手前から順番にresultsに入れます。

        if !results.isEmpty {
            let anchor = ARAnchor(name:"shipAnchor",transform:results.first!.worldTransform)
            //resultsがあれば、ARAnchorを作り、一番初め(手前)の特徴点の場所を代入します。
            sceneView.session.add(anchor: anchor)
           //ARanchor(shipAnchorという名前)を配置します。
        }
    }

4.ARanchor上にオブジェクトを表示します。

   // MARK: - ARSCNViewDelegate

    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    //Anchorが追加される度に呼び出されるrendererを追加します。
        if anchor.name == "shipAnchor" {
            guard let scene = SCNScene(named: "ship.scn",inDirectory: "art.scnassets") else { return }
            let shipNode = (scene.rootNode.childNode(withName: "ship", recursively: false))!
            node.addChildNode(shipNode)
            //追加されたARAnchorの名前が"shipAnchor"であれば、ARAnchor上にshipNodeを配置します。
        }
   }

■rendererとは?
ARSCNView上でイベントが発生した際に自動的に処理を行う関数です。
今回のようにARAnchorが追加された場合だけでなく、フレーム単位での処理やsessionを管理するものなどがあります。
参考:ARSCNViewDelegate 配下のAPIです。

なぜこの方法がふさわしいか

ARKit2では、"ARWorldMap"というARKitで取得した情報を他のデバイスと共有したり過去の情報を再現出来る方法が発表されました。
20180605174208.png

この際、ARAnchorは環境に紐づいた情報として記録し共有されるので、ある机の上にあったARAnchorは多少違う位置で復元しても同じ環境として認識されれば 同じ机の上に表示されます。
ARKit1時代の方法では、環境ではなくデバイスを中心とした位置関係で記録されているので、デバイスの1m前に置いたオブジェクトを記録して復元しても、復元した時の1m前に表示されていました。
また、他のデバイスと空間を共有するには、何か共通の基準となる物が必要となる為、自分で基準を作る(共有したい2つのiPhoneの場所を一度合わせる等)のでなければ、ARAnchorを利用する事が簡単かつ汎用性が高い手法となります。
参考:ARKitでグラフィティアートをして、ARWorldMapで共有する

ただし、そこまで環境と合致する必要がないオブジェクト(すぐ消えるシューティングの弾や、個人で表示したり再度表示しないモノなど)は、従来通りのARKit1の方法の方が良いです。

まとめ

ARKit2時代はこの方法がメインとなりました。
また、ImageTrackingやFaceTrackingも何か基準になるもの場所をARAnchorとして捉え、その上にオブジェクト表示する方法です。
ARKit3で出てきたCollaborative Sessionなども有効に活用する為には、この方法を基準に開発する必要があります。(私はARKit1をやっと理解した瞬間にARKit2の必要性が分かり手戻りした記憶があります。。)

次回は、2019年に出てきたRealityKitを使ったオブジェクト配置の方法を書こうと思います。ARKitのはじめかたシリーズは次で最終回です。

ここまで読んで頂きありがとうございました!

続きはこちら▶︎ ARKitのはじめかた その4「オブジェクトを配置する(ARKit3版)」

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

iOS13のMFMailComposeViewControllerにつまずいた

はじめに

もうネタがない...と思いつつ iOS13 の MFMailComposeViewController につまずいたという噂を聞いたので色々動かしたみました。

動かしてみたところナビゲーションバーのカスタマイズがうまくいかない:confused:

検証

AppDelegate で下記のように全体のナビゲーションバーをカスタムしてみる。

検証1

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  // Override point for customization after application launch.
  UINavigationBar.appearance().tintColor = .white
  UINavigationBar.appearance().barTintColor = .red
  UINavigationBar.appearance().titleTextAttributes = [.foregroundColor : UIColor.white]
  UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor : UIColor.white]
  return true
}

MFMailComposeViewController の表示は下記。

if MFMailComposeViewController.canSendMail() {
  let mailVC = MFMailComposeViewController()
  mailVC.mailComposeDelegate = self
  mailVC.setToRecipients(["piyo@example.com "])
  mailVC.setSubject("Singleton")
  mailVC.setMessageBody("しんぐるしんぐるとんとんとん", isHTML: false)
  present(mailVC, animated: true)
} else {
  print("Mail services are not available")
}

結果1

結果は下記のような表示。(実機が iPod touch と iPad しかないので大きさがちょっと...:see_no_evil:)

iOS12 iOS13
12_1 13_1

うーん両方思った結果とは違う...:neutral_face:

検証2

AppDelegate はそのまま MFMailComposeViewController の表示はをちょっと修正。

if MFMailComposeViewController.canSendMail() {
  let mailVC = MFMailComposeViewController()
  mailVC.navigationBar.tintColor = .white // ここ追加
  mailVC.navigationBar.titleTextAttributes = [.foregroundColor : UIColor.white] // ここ追加
  mailVC.navigationBar.largeTitleTextAttributes = [.foregroundColor : UIColor.white] // ここ追加
  mailVC.mailComposeDelegate = self
  mailVC.setToRecipients(["piyo@example.com "])
  mailVC.setSubject("Singleton")
  mailVC.setMessageBody("しんぐるしんぐるとんとんとん", isHTML: false)
  present(mailVC, animated: true)
} else {
  print("Mail services are not available")
}

結果2

結果は下記のような表示。

iOS12 iOS13
12_2 13_2

うーんボタンは変わったけど両方思った結果とは違う...:neutral_face:

iOS12 でタイトルの色が変わらない件に関しては下記がヒットした。が、解決策は見当たらず:neutral_face:

検証3

iOS12 のタイトルはもうあきらめよう:grimacing:

iOS13 をどうするか...??色々調べてみると iOS13 から UINavigationBarappearance の設定が変わった模様(参考

AppDelegate を下記のように書き換えます。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  // Override point for customization after application launch.
  if #available(iOS 13.0, *) {
    let appearance = UINavigationBarAppearance()
    let button = UIBarButtonItemAppearance(style: .plain)
    button.normal.titleTextAttributes = [.foregroundColor: UIColor.white]
    appearance.buttonAppearance = button
    let done = UIBarButtonItemAppearance(style: .done)
    done.normal.titleTextAttributes = [.foregroundColor: UIColor.white]
    appearance.doneButtonAppearance = done
    appearance.backgroundColor = .red
    appearance.titleTextAttributes = [.foregroundColor : UIColor.white]
    appearance.largeTitleTextAttributes = [.foregroundColor : UIColor.white]
    UINavigationBar.appearance().standardAppearance = appearance
    UINavigationBar.appearance().scrollEdgeAppearance = appearance
  } else {
    // Fallback on earlier versions
    UINavigationBar.appearance().tintColor = .white
    UINavigationBar.appearance().barTintColor = .red
    UINavigationBar.appearance().titleTextAttributes = [.foregroundColor : UIColor.white]
    UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor : UIColor.white]
  }
  return true
}

MFMailComposeViewController の表示はもちょっと修正。

if MFMailComposeViewController.canSendMail() {
  let mailVC = MFMailComposeViewController()
  if #available(iOS 13.0, *) {
  } else {
    mailVC.navigationBar.tintColor = .white
    mailVC.navigationBar.titleTextAttributes = [.foregroundColor : UIColor.white]
    mailVC.navigationBar.largeTitleTextAttributes = [.foregroundColor : UIColor.white]
  }
  mailVC.mailComposeDelegate = self
  mailVC.setToRecipients(["piyo@example.com "])
  mailVC.setSubject("Singleton")
  mailVC.setMessageBody("しんぐるしんぐるとんとんとん", isHTML: false)
  present(mailVC, animated: true)
} else {
  print("Mail services are not available")
}

結果3

結果は下記のような表示。

iOS12 iOS13
12_2 13_3

はい、だめーーー:neutral_face:

iOS13 が結局変わらない:neutral_face:

結論

MFMailComposeViewController のカスタマイズはあきらめよう!!ナビゲーションバーのカスタマイズはカスタムクラスなりで対応し MFMailComposeViewController に影響が出ないようにしよう:fist:

さいごに

appearanceMFMailComposeViewControllerUIActivityViewController などに思わぬ影響が出ていい思い出がないので個人的には撲滅したいです:expressionless:

(けど UIView.appearance().isExclusiveTouch = true はたまに使ったりします:speak_no_evil:)

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

Swift Network.framework Study 20191209「Handling Path Updates」

Study

Network.framework
Study:「Handling Path Updates」
下記メソッドを実装
内容はパラメータを表示

  • pathUpdateHandler
    ネットワークインタフェース(NWPath)変更時にコールされる
    最初は、コネクション生成時にコールされる様です
  • viabilityUpdateHandler
    送受信可能になるとコールされる
    NWState「ready」後にコールされる様です
  • betterPathUpdateHandler
    優先される代替えネットワークインタフェース(NWPath)時にコールされる
    試せていないです

環境

Client:Swift、Xcode
Server:Java、NetBeans 12/8と一緒のため割愛

Client Source Swift

main.swift

import Foundation
import Network

var sendAndReceive = SendAndReceive(host: "localhost", port: 7777, nWParameters: .tcp)

sendAndReceive.startConnection()

while sendAndReceive.running {
    sleep(1)
}

SendAndReceive.swift

import Foundation
import Network

class SendAndReceive {
    public var running = true
    private var host:NWEndpoint.Host
    private var port:NWEndpoint.Port
    private var nWParameters: NWParameters

    init(host:NWEndpoint.Host, port:NWEndpoint.Port, nWParameters: NWParameters) {
        self.host = host
        self.port = port
        self.nWParameters = nWParameters
    }

    func startConnection() {
        let myQueue = DispatchQueue(label: "ExampleNetwork")
        let connection = NWConnection(host: host, port: port, using: nWParameters)
        connection.pathUpdateHandler = { (nWPath) in
            print("pathUpdateHandler:\(nWPath)")
        }
        connection.viabilityUpdateHandler = { (viability) in
            print("viabilityUpdateHandler:\(viability)")
        }
        connection.betterPathUpdateHandler = {(better) in
            print("betterPathUpdateHandler:\(better)")
        }
        connection.stateUpdateHandler = { (newState) in
            switch(newState) {
            case .setup:
                print("setup")
            case .waiting(let error):
                print("waiting")
                print(error)
            case .preparing:
                print("preparing")
            case .ready:
                print("ready")
                self.sendMessage(connection)
            case .failed(let error):
                print("failed")
                print(error)
            case .cancelled:
                print("cancelled")
            @unknown default:
                print("defaults")
                break
            }
        }
        connection.start(queue: myQueue)
        self.receive(nWConnection: connection)
    }

    func sendMessage(_ connection: NWConnection) {
        let data = "Example Send Data".data(using: .utf8)
        let completion = NWConnection.SendCompletion.contentProcessed { (error: NWError?) in
            print("送信完了")
        }
        connection.send(content: data, completion: completion)
    }

    func receive(nWConnection:NWConnection) {
        nWConnection.receive(minimumIncompleteLength: 1, maximumLength: 5, completion: { (data, context, flag, error) in
            print("receiveMessage")
            if let data = data {
                let receiveData = [UInt8](data)
                print(receiveData)
                print(flag)
                if(flag == false) {
                    self.receive(nWConnection: nWConnection)
                }
            }
            else {
                print("receiveMessage data nil")
            }
        })
    }

}

Output(送信完了まで)

pathUpdateHandler:satisfied (Path is satisfied), interface: lo0
preparing
ready
viabilityUpdateHandler:true
送信完了
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリでよくある上タブ作ってみた

コード:https://github.com/Yaruki00/UnderBarTabView
PodsやCarthage対応はできてないので、もし使いたい場合はファイル引っこ抜いてください:crying_cat_face:
ほら、カスタマイズしやすいし・・・ね・・・:joy_cat:

機能

normal.gifinfinite.gif

  • 現在選択している箇所に下線がつきます。
  • タブのサイズは、固定と文字列によって可変の2通りから選べます。
  • 無限にループするか、しないかを選べます。
  • 余白やフォント、色を多少調整することができます。
  • 作ったのはタブの部分だけなので、コンテンツ部分やそれとの連携は自作する必要があります。

だいたいの作り

UICollectionViewをベースにしており、タブの分だけセルを生成し並べています。
無限スクロールはタブを3セット用意して、スクロールのアニメーション終わったタイミングでスクロール位置を真ん中のセットに戻すことで実現しています。
余白や色、フォント等の設定は別クラスになっており、セットアップ時に渡します。

無限スクロールの参考:
https://techblog.zozo.com/entry/tab_page_viewcontroller

既知の問題

セルの読み込みが遅いときがある

プログラムからタブをスクロールさせる場合(UICollectionViewscrollToItem(at:at:animated:)とか)、画面外のセルの読み込みが遅い場合があります。
どうもアニメーションが終わってから読み込みが走っているように見えますが、部分的にしか起きないしよくわからないです。わかる方いましたら是非教えて下さい!

参考(?):
https://living-sun.com/ja/swift/824352-in-auto-scrolling-uicollectionview-cellforitematindexpath-not-triggered-by-contentoffset-swift-uicollectionview-tvos.html

スクロールが部分的にぎこちない

無限ループするバージョンでは場所によってスクロールの速度が不自然な場合があります。
スクロール位置を調整するところでなにかおかしくなっていると思うのですが、ちゃんとした原因はまだつかめていないです。

役に立つかも知れない情報

セルの座標を取得する

下線を動かす時にセルの座標を取得していますが、UICollectionViewLayout
layoutAttributesForItem(at:)を使用することで取得することができます。

UICollectionViewのリロードが終わってから処理を行いたい

UIViewanimate(withDuration:animations:completion:)を使用することでリロードの完了を待つことができます。UICollectionViewのExtensionで書いておくと便利かも知れません。

UIView.animate(
    withDuration: 0.0,
    animations: {
        self.collectionView.reloadData()
    },
    completion: { _ in
        // reloadData()後に行いたい処理
    }
)

参考:
https://qiita.com/ponkichi4/items/d5d46556773a6bc98f9c

おわりに

この手のライブラリは探せばたくさん見つかるのですが、勉強も兼ねて自作してみました?
シンプルな機能で良いのであれば自作してもそこまで手間ではないのでアリかなと思いました?

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

Swift:自作CocoaPodsライブラリでAssets Catalogを使う

1.自作フレームワーク内に.xcassetsファイルを作成
2.普通にリソースを加える
3.Assets Catalogからリソースを取得

画像を取得する例
public func bundleImage(name: String) -> NSImage? {
    // ↓ライブラリのデモTarget用
    var bundle = Bundle(identifier: "ライブラリTargetのBundle Identifier")
    // ↓ライブラリをimportした時用
    if bundle == nil {
        bundle = Bundle(for: このメソッドがあるクラス名.self)
    }
    return bundle?.image(forResource: NSImage.Name(name))
}

4..podspecにリソースを使うことを書き加える

.podspec
spec.resources = "ライブラリ名/**/*.xcassets"

↑ライブラリ名と言うのはつまりプロジェクトのルートフォルダのことを指します。

参考

How to use images asset catalog in cocoapod library for iOS

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

collectionViewのReadableContentView(FollowReadableWidth)が機能しない場合

collectionViewに直接FollowReadableWidthを設定するのではなく、collectionViewの下にViewを敷いてViewに対してFollowReadableWidthを設定してみよう。

① viewを配置
② viewのAutoLayoutはConstrain to Marginsにチェックを入れて 0,0,0,0
③ viewに対してFollowReadableWidthのチェックをON
④ collectionViewを配置してcollectionViewのAutoLayoutは Constrain to Marginsにチェックを入れて 0,0,0,0
⑤ あとはいつも通りcollectionViewをセットするだけ。

[FollowReadableWidth]
 201<img width=

[Constrain to Margins]
 2019-12-09 17.45.15.png

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

extensionをふんだんに使ってコードを整理しよう

iOS Advent Calendar 2019も折り返しに来ました。担当の@417_72kiです。

protocol毎にextensionでコードブロックを分けるという有名(?)なhackがありますが、
今回はprotocol以外でも積極的にextensionで分けていこうぜ!っていう記事を書きました。

注意

どう呼べばいいか困ったものについて、この記事では以下のように定義しています。

  • definition block -> class/struct/enum宣言したブロック( class Hoge {~}で囲まれたブロック )
  • extension block -> extension宣言したブロック ( extension Hoge {~} で囲まれたブロック )

以下、上記の言葉は太字で記述していきます。

ベタ書きされたコード

例としてこんなFatViewControllerを考えます(アーキテクチャの話は一旦無視します)。

ベタ書きされたFatViewController
FatViewController.swift
class FatViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    private var _items: [Item]?

    var items: [Item] {
        get { _items ?? [] }
        set { _items = newValue }
    }

    var isEmpty: Bool { items.isEmpty }

    @IBOutlet private weak var tableView: UITableView!
    @IBOutlet private weak var button: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        setup()
    }

    private func setup() {
        tableView.dataSource = self
        tableView.delegate = self
    }

    private func doSomething(with item: Item) {
        print(item.name)
    }

    func reloadView() {
        tableView.reloadData()
    }

    // MARK: - UITableViewDataSource
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        cell.textLabel?.text = items[indexPath.row].name

        return cell
    }

    // MARK: - UITableViewDelegate
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        doSomething(with: items[indexPath.row])
    }

}

ステップ1: protocolを分割する

これについては参考記事があるのでここでの解説は割愛します。

protocol分割されたFatViewController
FatViewController.swift
class FatViewController: UIViewController {
    private var _items: [Item]?

    var items: [Item] {
        get { _items ?? [] }
        set { _items = newValue }
    }

    var isEmpty: Bool { items.isEmpty }

    @IBOutlet private weak var tableView: UITableView!
    @IBOutlet private weak var button: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        setup()
    }

    private func setup() {
        tableView.dataSource = self
        tableView.delegate = self
    }

    private func doSomething(with item: Item) {
        print(item.name)
    }

    func reloadView() {
        tableView.reloadData()
    }
}

// MARK: - UITableViewDataSource
extension FatViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        cell.textLabel?.text = items[indexPath.row].name

        return cell
    }
}

// MARK: - UITableViewDelegate
extension FatViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        doSomething(with: items[indexPath.row])
    }
}

ちなみに、protocol functionextensionに切り出したら// MARK: - ~を消している記事をよく見かけますが、筆者はあえて各extension blockに対して// MARK: - ~を付けるようにしています。
理由は後述します。

ステップ2: functionを分割する

functionは基本的にextensionに切り出すことができます。
classにおけるoverride functionは例外で、definition blockにしか定義できないため、// MARK:で区切ります。

function分割されたFatViewController
FatViewController.swift
class FatViewController: UIViewController {
    private var _items: [Item]?

    var items: [Item] {
        get { _items ?? [] }
        set { _items = newValue }
    }

    var isEmpty: Bool { items.isEmpty }

    @IBOutlet private weak var tableView: UITableView!
    @IBOutlet private weak var button: UIButton!

    // MARK: Life cycles
    override func viewDidLoad() {
        super.viewDidLoad()

        setup()
    }
}

// MARK: - Functions
extension FatViewController {
    private func setup() {
        tableView.dataSource = self
        tableView.delegate = self
    }

    private func doSomething(with item: Item) {
        print(item.name)
    }

    func reloadView() {
        tableView.reloadData()
    }
}

// MARK: - UITableViewDataSource
extension FatViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        cell.textLabel?.text = items[indexPath.row].name

        return cell
    }
}

// MARK: - UITableViewDelegate
extension FatViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        doSomething(with: items[indexPath.row])
    }
}

ステップ3: computed propertyを分割する

propertyのうち、computed propertyextensionに切り出すことができます。
stored propertydefinition blockにしか定義できないため、必要に応じて// MARK:で区切ったりします。

computed property分割されたFatViewController
FatViewController.swift
class FatViewController: UIViewController {
    // MARK: Private properties
    private var _items: [Item]?

    // MARK: Outlets
    @IBOutlet private weak var tableView: UITableView!
    @IBOutlet private weak var button: UIButton!

    // MARK: Life cycles
    override func viewDidLoad() {
        super.viewDidLoad()

        setup()
    }
}

// MARK: - Computed properties
extension FatViewController {
    var items: [Item] {
        get { _items ?? [] }
        set { _items = newValue }
    }

    var isEmpty: Bool { items.isEmpty }
}

// MARK: - Functions
extension FatViewController {
    private func setup() {
        tableView.dataSource = self
        tableView.delegate = self
    }

    private func doSomething(with item: Item) {
        print(item.name)
    }

    func reloadView() {
        tableView.reloadData()
    }
}

// MARK: - UITableViewDataSource
extension FatViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        cell.textLabel?.text = items[indexPath.row].name

        return cell
    }
}

// MARK: - UITableViewDelegate
extension FatViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        doSomething(with: items[indexPath.row])
    }
}

ステップ4: accessibleごとに分割する

extensionにもAccess Levelを設定することができます。
ブロック内のAccess Levelはブロック本体のAccess Level以下になります
(ただし、トップレベルのブロックにおけるprivatefileprivateと同義になります)。

そこで、分割したextensionを更にAccess Levelごとに分割することで、ブロック単位でAccess Levelを設定できるとともにprivateの付け忘れから開放されます。

accessibleごとに分割されたFatViewController

privatecomputed propertyの良い例が思いつかなかったため、ここではfunctionだけ対応しています
FatViewController.swift
class FatViewController: UIViewController {

    // MARK: Private properties
    private var _items: [Item]?

    // MARK: Outlets
    @IBOutlet private weak var tableView: UITableView!
    @IBOutlet private weak var button: UIButton!

    // MARK: Life cycles
    override func viewDidLoad() {
        super.viewDidLoad()

        setup()
    }
}

// MARK: - Computed properties
extension FatViewController {
    var items: [Item] {
        get { _items ?? [] }
        set { _items = newValue }
    }

    var isEmpty: Bool { items.isEmpty }
}

// MARK: - Public Functions
extension FatViewController {
    func reloadView() {
        tableView.reloadData()
    }
}

// MARK: - Private Functions
private extension FatViewController {
    func setup() {
        tableView.dataSource = self
        tableView.delegate = self
    }

    func doSomething(with item: Item) {
        print(item.name)
    }
}

// MARK: - UITableViewDataSource
extension FatViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

        cell.textLabel?.text = items[indexPath.row].name

        return cell
    }
}

// MARK: - UITableViewDelegate
extension FatViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        doSomething(with: items[indexPath.row])
    }
}

他にextensionに切り出せるもの

クラス定数(static let)
FatViewController.swift
// MARK: - Constants
extension FatViewController {
    static let initialItems: [Item] = [] // -> OK
    let initialItems: [Item] = [] // -> NG
}

同じ定数でもインスタンス定数の方はextensionに切り出せません。
不思議ですね(棒

inner class/struct/enum
FatViewController.swift
// MARK: - State
extension FatViewController {
    enum State {
        case hoge
        case fuga
        case foo
        case bar
    }
}

// MARK: -
extension FatViewController.State {
    var isHoge: Bool { self == .hoge }
}

もちろんネストされた型に対してもextensionを貼ることができますし、
その中に更にネスト型を定義することもできます。
ブロックのネストを増やすことなく型のネストを増やせるので、複雑なJSONからCodableなstructを組み立てる時に重宝します。

ちなみに、BuildConfig.swiftという自作ツールで生成されるSwiftファイルもこの手法を使っています。
よかったら実際に使ってみて生成されたコードを見てみてください(宣伝

その他

実装する機会が減ってきたのでここでは触れませんが、convenience initializerもextensionで定義することができます。

// MARK:とパンくずリストとMinimap

Xcodeのエディタ領域上部にあるパンくずリストで、ファイル名の次の要素を開くとDocument Itemsが表示されます。
(^+6でも開きます)

// MARK: 見出し名を使うことでこのDocument Itemsに見出しを付けることができます。
また、 MARK: - 見出し名とすることで、見出しの前に区切り線を付けることができます。
image.png

更に、Xcode11で登場したminimapでは// MARK: 見出し名を付けた所に見出しが表示されるようになります。
Document Itemsと同様、こちらも-付きMARKにすると区切り線が付きます。

image.png

先述の、筆者が全ブロックにMARK: - 見出し名を付ける理由がこれです。
見出しが付くおかげでコードの構造がパッと見で分かるようになりますね。

まとめ

extensionを活用してコードブロックを整理することで、各ブロックに役割を持たせる事ができます。
また、MARKコメントとminimapとの組み合わせでコードの見通しも良くなってDXも爆アゲです。

この1年ずっとこの手法を使っていますが今の所デメリットが見つかっていないので、
誰かこの手法で困ったことがあったら教えていただきたいです(※他の言語ではできないみたいなのは除く)。

それでは皆様、良いお年を!✋

参考

Using Swift Extensions The “Wrong” Way - Natasha The Robot(@NatashaTheRobot)
【Swift】Protocolごとにextensionで切り分けて実装するワケ(@ktanaka117)
[Xcode 8] Swiftのドキュメントコメントについての簡潔なまとめ(@y-some)
What's New in Xcode 11(@akatsuki174)

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

iOSでもマルチモジュール化したい!

この記事は iOS #2 Advent Calendar 2019 の12日目です。
今年の1年間、ぼくが興味を持ち続けてきたiOSアプリのマルチモジュール化について、検討したことをここに書き出します :slight_smile:

マルチモジュール化とは

タイトルからさらっと「マルチモジュール化」という言葉を使ってしまいましたが、ここでは、ライブラリの仕組みを使って複数のモジュールで構成されたiOSアプリにすることを、マルチモジュール化と呼ぶことにします。逆にマルチモジュール化していないアプリをモノリシックなアプリと呼ぶことにします。

ライブラリと言えば、OSSとして公開されている汎用的で再利用可能なものが真っ先に頭に浮かびますが、ここではそういったものを使っているどうかは関係なくて、開発中のアプリの処理を積極的にライブラリとして切り出して(=モジュール化)、そのアプリ内で利用してゆくことを考えます。

イメージ

モジュールの分割方針

モジュールに切り出すと言っても、どこをどう切り出せばいいでしょうか。

ここでSwiftの言語特性を考えてみます。Swiftのアクセスコントロールにはモジュールを境界とするもの(open, public)があるので、モジュールの外に見せる機能とモジュール内だけで隠す実装をコードレベルでコントロールできます。ですから、各モジュールごとに独立した責務を持たせるようにし、アクセスコントロールを使って独立性を高める、という方針で分割すると良いでしょう。

具体的に、どういった単位で分割するのが良いかというのは、アプリの設計と密接に絡む問題…というより設計そのものなので、一般的な答えを出すことはできません。ですが、後に述べるメリット・デメリットを考えながら、いくつかの方向性は考えられます。

機能単位で分割

アプリがある程度の独立した複数の機能から構成されているのなら、その機能単位でモジュールにするというのは直感的でわかりやすい分割方針です。

機能単位での分割例

レイヤー単位で分割

MVVM、MVP、Clean Architecture、VIPER、…などの設計パターンを用いるのなら、そのレイヤー単位でモジュールを分割するのも良いと思います。こういった設計パターンに共通する根本の考え方は、各レイヤーで責務を分離し、それぞれを疎結合にしておくことです。ですから、レイヤーでモジュールを分割するというのは意に適っています。

レイヤー単位での分割例

機能×レイヤー

上記の組み合わせで、大きくは機能で分割し、そこから必要に応じてレイヤーに分割するというのもあると思います。モジュール数が増えることになりますが、アプリの規模が大きいのならそれは自然なことだと思います。逆に規模の小さいアプリで無理して分割するのはオーバーキル感があるので、そこはアプリに合わせて調整が必要です。

いろんな分割例

モジュールに分ける意味のひとつが責務の分割です。責務を分割しておけば、他への影響を最小限に抑えてモジュールを更新できるので、最初は機能で分割しておいて、ある機能の規模が大きくなってきたところで、それをさらに分割するというのも良いと思います。

マルチモジュール化のメリット

ぼくがマルチモジュール化のメリットとして期待している点は、次の3つです。

  • 開発時間の短縮
  • 影響範囲の最小化
  • コンフリクトの減少

開発時間の短縮

モジュールごとに、テストターゲットを用意したり、そのモジュールの動作を確認するための小さなアプリターゲットを用意することで、そのモジュールだけをビルドして開発を行うことができるようになると考えています。

何よりも、モジュールだけのコンパイルで済むのでビルド時間が短縮されますが、テストに関してもそのモジュールのものだけを走らせることでその時間が短縮されます。機能の実装や修正を行う際には、何度もビルド&確認を繰り返すので、アプリの規模がある程度大きくなると、その効果は大きいと思います。

コード変更時の、アプリ全体の差分ビルドも、もしかすると速くなるかもしれないのですが、その点は検証できていません。

影響範囲の最小化

コンポーネントが相互に依存し合っていたりして密に結合した状態になっていると、どこかを変更する際の影響範囲が広がってしまい、変更作業が大きくなったり、気づかないバグを生みやすくなります。

疎結合なコンポーネントの集まりとしてうまく設計していれば、モノリシックなアプリであってもどこかを変更する際の影響範囲を小さくできます。
しかし、本来は特定の機能の実装だけのために作ってあったはずのクラスが他の実装でも使われてしまったり、コンポーネント内でのみ利用するためのメソッドが気づけば外から呼ばれてしまっていたりといった事故は起こり得ます。特に、チームに複数の開発者がいる場合は、意思疎通のズレによってそういうことが起こりやすくなります。

モジュール化すれば、 openpublic 以外のアクセス修飾子を持つクラスやメソッドは、モジュール外には隠されます。アクセス修飾子をつけなければ internal として扱われるので、意識的にアクセス修飾子で指定したものだけがモジュール外に公開されるようになります。
そもそも、そのモジュールをimportしなければ、公開されているものを使うこともできません。
意図せず誤って使ってしまうことがない、ということをコンパイラに保証してもらえるので安心感があります。

また、モジュールが他のモジュールに依存する際には、単方向の依存になります。モジュール間の依存関係もはっきりさせることができます。

コンフリクトの減少

これはやや副次的な効果ですが、モジュールごとにプロジェクトファイルを分けておけば、プロジェクトファイル変更時(ファイル追加など)のコンフリクトが少なくなるのではないかと思っています。

マルチモジュール化のデメリット

もちろん、マルチモジュール化することのデメリットもあります。対象のアプリの機能性・規模によっては、マルチモジュール化のデメリットの方が大きくなる場合もあるので、少なくとも、最初から完璧なものを求めるのはやめた方がいいと思います。

モジュール分割の難しさ

前にも書いたとおり、モジュールをどう分割するかの一般的な解はありません。また、抽象化を突き詰めて分割しすぎると、モジュール同士の依存関係が複雑になって、モジュールを管理するのが難しくなります。
分割しない場合は考えなくてもよかったことを考えなければいけない分、手間が増えるのは間違いないと思います。

抽象度が高くなる分、複雑になる

マルチモジュールのメリットを活用しようとすると、モジュール内の実装は隠して、提供する機能だけを外側に見せるようにしたくなります。つまり、モジュールが提供するインタフェースの抽象度を高くすることになります。

また、モジュールの依存関係は循環しないようにする必要があるので、モジュールが相互に依存しあうことはできません。これを解決するために外側から依存を注入する方法を使うことになるでしょう。
例えば、モジュールAが外部から提供してほしい機能をプロトコルとして定義し、モジュールBでそのプロトコルに準拠したクラスを作成、インスタンス化して、モジュールAに引き渡すというものです。この場合、モジュールAはモジュールBの存在を知らなくてよいので、モジュールAからモジュールBへの依存は必要ありません。

このように抽象度をあげて結合を疎にすることは、実際の処理がどこに実装されているのかがわかりにくくなるという副作用があります。モジュール間を接続するコードも増えるので、必要以上に抽象度を上げることは、ただ単にプログラムを複雑にしてしまう危険性があります。

2つのライブラリの種類

モジュールを作成する際に、Xcodeで新規プロジェクト(または新規ターゲット)としてサクッと作ることのできるライブラリには次の2種類があります。

  • Framework
  • Static Library

ライブラリの種類

Framework

フレームワークは、ライブラリとその他のリソースを1つにまとめたものです。

Dynamicフレームワーク

これを選択して作られるフレームワークにはDynamicライブラリが入っているので、種別としてはDynamicフレームワークです。Dynamicフレームワークは、Embedded Framework(アプリにくっついていくフレームワーク)としてiOS 8から使えるようになりました。
なお、Carthageが扱うのもDynamicフレームワーク、CocoaPodsで use_frameworks! をつけたときもDynamicフレームワークが使われます。

Dynamicライブラリはアプリのメインターゲット(アプリのプログラム)とは別のファイルとして分かれていて、実行時に動的にロードされます。

Dynamicフレームワーク

macOSでは、特定の機能でしか使わないモジュールをDynamicライブラリにすることで、実際に必要になるまでそれをロードしないようにできます。そうすることで起動時間を短縮できます。また、Dynamicライブラリをプラグインのように追加したり差し替えたりすることができます。
しかし、iOSでは起動時にすべてのDynamicライブラリがロードされます。また、ストアに申請するアプリにくっついていく形式しかサポートされていないため、Dynamicライブラリだけを差し替えたりはできません。
ですから、残念ながら、macOSの場合のような利点はありません。

むしろ、Dynamicライブラリの数が多いと逆に起動時間が遅くなります。実際、 WWDC 2016のセッション ではDynamicフレームワークは6個程度にとどめるように案内されています。

So you absolutely can and should use some, but it's good to try to target a limited number, we would, I would say off hand, a good target's about a half a dozen.

翌年の WWDC 2017のセッション でも、できるだけ少なくするようアドバイスされています。

So last year I said do less, and I'm going to say that again this year and I'm always going to say that because the less you do, the faster we can launch.

And no matter how much we speed things up, if we have less work, it's going to go faster.

And the advice is basically the same. You should use fewer dylibs, if you can, you should embed fewer dylibs.

Dynamicフレームワークが使えるようになったiOS 8では、同時にExtension(Today Extensionや、Share Extensionなど)が登場しました。
アプリ本体(メインターゲット)とExtensionで共通する処理を、Dynamicフレームワークにすることにより、アプリサイズの肥大化を抑えられます。おそらく、iOSのDynamicフレームワークはこの使い方を主な目的としているのでしょう。

アプリサイズの削減

モジュールの数が多くなりそうなら、Dynamicフレームワークは避けた方がいいでしょう。

Static Library

SwiftはXcode 6で登場しましたが、SwiftのStaticライブラリはXcode 9になって、やっと作れるようになりました。

Staticライブラリは、ビルド時のリンクフェーズでアプリのメインターゲットに取り込まれます。

Staticライブラリ

このため、Dynamicフレームワークと違って、出来上がる構成はモノリシックなアプリの場合と同じです。起動速度の低下も意識する必要はありません。

ただ、Staticライブラリはフレームワークと違ってライブラリそのものです。つまり、リソースを持つことができません。UIに絡む処理をモジュールで提供したい場合は、Storyboardや画像、文字列など、関連するリソースをモジュールに含めたいところですが、残念なことにそれができません。

リソースについてはメインターゲットに持たせるか、Dynamicフレームワークを使うことになるでしょう。

プロジェクトの構成

モジュールを含むXcodeプロジェクトの構成について考えます。方法はいろいろあるのですが、そのうちのいくつかを紹介します。

1プロジェクト内にターゲットを追加する構成

プロジェクトは1つで、その中に、モジュールのターゲットを追加する構成です。

マルチターゲット構成

シンプルで良い構成だと思います。

この構成で注意したいところは「ターゲットを間違えないこと」です。
ファイルを追加するときに、正しいターゲットにチェックが入っていることを確認したり、ファイルを編集しているときについうっかり右のInspectorでターゲットを変更しないように注意しましょう。

multi_target2.jpg  multi_target3.jpg

なお、この構成ではプロジェクトファイルは1つになるので、メリットとして挙げた「コンフリクトの減少」は期待できません。

これらのターゲット間違いやコンフリクトを避けるために、XcodeGenを使うという解決策が、以下のクックパッドさんのブログで紹介されています。

XcodeGenによる新時代のiOSプロジェクト管理
https://techlife.cookpad.com/entry/2019/04/26/110000

サブプロジェクトの利用

モジュールごとにプロジェクトファイルを分けて、依存するプロジェクトをサブプロジェクトとして参照する構成です。

サブプロジェクト

各モジュールのプロジェクトが分かれているので、モジュール単体のプロジェクトを開いて開発を行うこともできます。
また、モジュール単体でプロジェクトになっているので、アプリのプロジェクトと完全に独立した別のプロジェクトからもそのモジュールをサブプロジェクトとして参照できます。ですから、アプリのプロジェクトには影響を与えずに、そのモジュール専用のテストアプリを作ったりすることもできます。

この構成の注意点は、プロジェクト設定がモジュールごとに分かれることです。アプリ全体でコンパイルオプションを変更したい場合は、それぞれのモジュールのプロジェクト設定を変更してまわる必要があります。

パッケージマネージャの利用

各モジュールをCocoaPods、Carthage、Swift Package Manager(以下SwiftPM)といったパッケージマネージャに対応するパッケージとして開発する方法もできると思います。
CocoaPodsは プライベートなSpec Repoを作ることができますし 、CarthageやSwiftPMでもプライベートなgitリポジトリを参照できるので、OSSとして公開するつもりがなくてもこれらのパッケージを作って使うことは可能です。

しかし、パッケージ開発の手順が必要な分、どうしても煩雑になるので、この選択はちょっと微妙かなと思っています。

ただ、SwiftPMには、近い将来、 Staticライブラリであってもリソースを使えるようになる可能性 があるので、ちょっと期待しています。

まとめ&参考資料

iOSアプリのマルチモジュール化について、

  • モジュールの切り出し方法
  • メリット・デメリット
  • ライブラリの種類の違い
  • プロジェクト構成

という視点で、これまでに検討してきた内容を書きました。
と言っても、検討結果を晒しただけであって、実践はまだそれほど進んでいないのが現状です。実際にマルチモジュール化してみると新たな別の発見があるかもしれないと思っています。

そもそもは、DroidKaigi 2018で「マルチモジュールのすゝめ」というセッションを聞いて、Androidではそういう開発手法の波があるのかと知りました。

今年になって、DroidKaigi 2019の「multi-module Androidアプリケーション」というセッションを見て、iOSでも同じことができないだろうかと考え始めました。このセッションでは40個のモジュールで構成されたプロジェクトが紹介されていました(ちなみに、DroidKaigi 2019ではこの他にもマルチモジュール絡みのセッションが多かった印象)。

その少し後に、Cookpad TechConf 2019で「〜霞が関〜 クックパッドiOSアプリの破壊と創造、そして未来」というセッションがあったことを知り、iOSでもマルチモジュール化の波は来てるじゃん!と確信しました。

ということで、これまでの検討では先人が公開してくれた下記の資料の影響を受けています :slight_smile:

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

[初心者向け]Custom Classが追加できない場合

Xcodeで画面遷移を作る際、Custom Classに自分が作成したクラスが出てこない
スクリーンショット 2019-12-09 11.32.59.png

解決方法

画像の所(Setting View Controller)をクリックする
スクリーンショット 2019-12-09 11.43.53.png

画面右側のプレビューからクリックして選択しているとViewが選択された状態になってしまうため、自分が作ったクラスが出てこない!ってなる(なってた)

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

忘備録-Swiftの拡張

趣味でIOSアプリ開発をかじっていた自分が、改めてSwiftを勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

参考文献

この記事は以下の書籍の情報を参考にして執筆しました。

拡張の概要

あるクラス、構造体、列挙型、プロトコルに新しい機能を追加することができる。
エクステンションと呼ぶこともある

拡張定義に追加できる定義

・計算型のインスタンスプロパティ
・格納型のタイプメソッド、計算型のタイププロパティ
・インスタンスメソッド、タイプメソッド
・イニシャライザ
・添字付け
・ネスト型定義(型はその拡張定義内でのみ使用する)

追加できないのは、格納型のインスタンスプロパティ、プロパティオブザーバ。
またそのクラスや他の拡張定義内に記述されている内容を上書きすることはできない。
宣言にfinalを指定することもできない。

既存の型に対する拡張定義

extension String {
    func toHoge() -> String{
        "hoge"
    }
}

let fuga = "fuga"
print(fuga.toHoge())        // hoge
print(fuga)        // fuga

拡張定義とイニシャライザ

拡張定義にイニシャライザを定義することは可能。しかしイニシャライザの定義には注意が必要。

クラス
指定イニシャライザ、デイイニシャライザは記述できない。
簡易イニシャライザは定義できる。
構造体
本体を宣言したモジュール別ルにコンパイルされる拡張定義にイニシャライザを書くという条件付きで利用できる。
selfを利用する前あるいはイニシャライザを終える前にself.init{}の呼び出しを行わないといけない。
本体にイニシャライザの記述がなく、既定・全項目イニシャライザを使っている場合に、
拡張定義にイニシャライザを定義できるが、既定・全項目イニシャライザを使い続けることができてしまう。

拡張定義と継承

拡張定義を追加したクラスのサブクラスにも定義は継承されるが、サブクラスで拡張定義の上書きはできない。
また、スーパクラスの定義をサブクラスの拡張定義で上書きすることはできない。

拡張定義とプロトコル

拡張定義にプロトコルを採用することができる。
プロトコルを採用する場合、プロトコルに適応する必要がある。

class Hoge {
    let hoge = "hoge"
}
extension Hoge: CustomStringConvertible{
    var description: String {
        "extension " + self.hoge
    }
}


let hoge = Hoge()
print(hoge)        //extension hoge

すでに適応している場合は宣言するだけでいい。

class Hoge {
    let hoge = "hoge"
    static func == (lhs: Hoge, rhs: Hoge) -> Bool {        // Equatableの適合条件
        lhs.hoge == rhs.hoge
    }
}
extension Hoge: CustomStringConvertible, Equatable{
// 略
}

プロトコル拡張

メソッド、計算型プロパティ、添字付けの実装を記述。
通常のプロトコルでする宣言、格納型のタイププロパティ、格納型のインスタンスプロパティを記述できない。
プロトコル拡張そのものにプロトコルの継承は行えない。

プロトコルを採用したデータ型はプロトコル拡張に記述されている実装を使用できる。
プロトコル拡張で定義された実装を既定定義という。

protocol ProtocolHoge {
    var hoge: String { get }
    var toArray: [String] { get }
}

extension ProtocolHoge{
    var toArray: [String] {
        let array: [String] = [hoge]
        return array
    }
}

struct Hoge: ProtocolHoge{
    let hoge = "hoge"
}

let hoge = Hoge()
print(hoge.toArray)        //["hoge"]

プロトコル拡張の制約

付属型 : プロトコル
付属型 = 型

protocol SimpleVector {
        associatedtype Element        //xとyの型を統一する
        var x : Element { get set }
        var y : Element { get set }
}

extension SimpleVector where Element == String{
    func toString() -> String { x + y }
}

struct VectorGrade : SimpleVector {
        var x, y: String
}

var hoge = VectorGrade(x:"A" , y:"B")
print(hoge.toString())        // AB

集合型

Set<T>

let hoge: Set<String> = ["apple", "orange", "peach"]
for element in hoge{
    print(element)        // 実行するたびに表示順が異なる
}

集合演算

和集合などの集合演算はプロトコルSetAlgebraで定義されている。
Set型はこれに適合している。

集合演算メソッド
和集合 : A.union(B) -> Set , A.formUnion(B)
差集合 : A.subtracting(B) -> Set , A.subtract(b)
積集合 : A.intersection(B) -> Set , A.formIntersection(B)
対称差集合 : A.symmetricDifference -> Set , A.formSymmetricDifference(B)

var hoge: Set<String> = ["a", "c", "d" , "e"]
let fuga = ["b", "c"]
let union = hoge.union(fuga)
print(union)        // ["a", "e", "b", "c", "d"]
hoge.subtract(fuga)
print(hoge)        // ["a", "e", "d"]

プロトコルOptionSet

swiftでは何らかの情報の組合せを表すのにプロトコルOptionSetを使う。

struct RoomOption : OptionSet {
    typealias RawValue = Int
    let rawValue: Int
    static let internet = RoomOption(rawValue: 0x001)
    static let breakfast = RoomOption(rawValue: 0x002)
    static let fullbath = RoomOption(rawValue: 0x004)
    static let all = RoomOption(rawValue: 0x007)
}

var room: RoomOption = [.internet, .fullbath]
print(room.rawValue)    // 5
if !room.contains(.breakfast){
    room.insert(.breakfast)
}
print(room.rawValue)    // 7

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

忘備録-Swiftのメモリ管理

趣味でIOSアプリ開発をかじっていた自分が、改めてSwiftを勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

参考文献

この記事は以下の書籍の情報を参考にして執筆しました。

リファレンスカウンタの概念

例えばDogクラスのインスタンスを参照する2つの変数があるとする。

class Animal: CustomStringConvertible{
  var weight: Int
  class var className: String {"Animal"}

  init(w: Int){
    weight = w
  }
  var description: String{
    Self.className
  }
}

class Dog: Animal {
  var name: String = ""

  init(n:String, w:Int) {
    super.init(w: w)
    name = n
  }

  override class var className: String {"Dog"}

  override var description: String{
    name
  }

  deinit {
    print("\(name) : deinit")
  }
}

// ①
var pochi: Dog! = Dog(n: "Pochi", w: 5)
print(pochi!)    // pochi
// ②
var x: Dog! = pochi    // インスタンスの参照を渡している。
x.name = "x"
// 同じインスタンスを指しているか
print(pochi === x)    // true
print(pochi!)
// ③
pochi = nil    // 何も表示されないのでdeinitは呼ばれていない
print(pochi === x)    // false
// ④
x = nil    // x : deinit // 全ての参照してる変数がなくなったので呼ばれた。

この時
①の時点では、変数pochiだけがインスタンスを参照するので、リファレンスカウンタは1。
②の時点では、変数pochiとxがインスタンスを参照するので、リファレンスカウンタは2。
③の時点では、pochiがnilを参照することになるので、リファレンスカウンタは1。
④の時点でxもnilを指すのでリファレンスカウンタが0になる。この時点で参照する変数がなくなり、インスタンスは解放される。

ARCによるメモリ管理

ARC (Automatic Reference Counting) :
リファレンス値を増減させることによってメモリを管理する。
リファレンスカウンタをいつ増加させるかはコンパイル時に決定できる。
インスタンスが不要になるタイミングがコード上で分かっているため、処理が高速でdメモリか開放に伴う不可の集中も起きにくい。

強い参照の循環

強い参照の循環(環状参照) : あるインスタンスが他のインスタンスを参照して、そのインスタンスから参照が帰ってくる、関係が環状になっているとメモリが解放されない。

A→B→C→D OK
A→B→C→A… NG
A→B→C→B… NG

class Student: CustomStringConvertible {
  let name: String
  var club: Club? = nil

  init(n: String){
    name = n
  }

  var description: String{
    "\(name): \(club?.name ?? "帰宅部")所属"
  }

  deinit {
    print("\(name) : deinit")
  }
}

class Club: CustomStringConvertible {
  let name: String
  var members = [Student]()

  init(n: String){
    name = n
  }

  func add(_ s: Student){
    members.append(s)
    s.club = self
  }

  var description: String{
    "\(name): \(members.count)人"
  }

  deinit {
    print("\(name) : deinit")
  }
}

do {
  let hoge: Club = Club(n: "hoge部")
  let taro: Student = Student(n: "taro")
  print(hoge)    // hoge部: 0人
  print(taro)    // taro: 帰宅部所属
// taro : deinit
// hoge部 : deinit
}
print("終わり")    // 終わり

addメソッドを実行して互いに参照させてみるとインスタンスの参照が残ったままになるのでdeinitが実行されていないことがわかる。

do {
  let hoge: Club = Club(n: "hoge部")
  let taro: Student = Student(n: "taro")
  hoge.add(taro)
  print(hoge)    // hoge部: 0人
  print(taro)    // taro: 帰宅部所属
}
print("終わり”””)    // 終わり

弱い参照

通常の変数や定数からクラスのインスタンスを参照すると、リファレンスカウンタの値が1増加。
弱い参照 : リファレンスカウンタの値が増えない参照方法。変数をweakというキーワードで修飾。参照していたインスタンスが解放されるとnilが代入される(ゼロ化される)ので変数かつオプショナル型で宣言しなければならない。

強い参照の循環で使用した例で適応してみる。
class StudentのClubクラスへの参照を弱い参照に変更する。
実行するとインスタンスが解放されていることが確認できた。

class Student: CustomStringConvertible {
  let name: String
  weak var club: Club? = nil    //ここを変えるだけ

  init(n: String){
    name = n
  }

  var description: String{
    "\(name): \(club?.name ?? "帰宅部")所属"
  }

  deinit {
    print("\(name) : deinit")
  }
}

//Clubクラスは変更なし//

do {
  let hoge: Club = Club(n: "hoge部")
  let taro: Student = Student(n: "taro")
  hoge.add(taro)
  print(hoge)    // hoge部: 1人
  print(taro)    // taro: hoge部所属
}
// taro : deinit
// hoge部 : deinit
print("終わり")    // 終わり

非所有参照

非所有参照 : クラスのインスタンスを参照しても所有権を主張せず、リファレンスカウンタを増やすことがない。unownedという修飾語をつける。
弱い参照と違う点
・常に何かのインスタンスを参照し続け、nilを値とはしない。
・参照してたインスタンスが解放されてもnilを代入しない。(ゼロ化しない)
・解放した後、誤ってアクセスすると実行時エラーとなるので注意が必要。
・値を自動的に変更しないので定数に指定することも可能
・ゼロ化するときはオーバヘッドになる(コストがかかる)が、ゼロ化しないので高速。

先ほど書いた弱い参照を置き換えた。
弱い参照と同様にインスタンスが解放されていることが確認できた。

class Student: CustomStringConvertible {
  let name: String
  unowned var club: Club? = nil

  init(n: String){
    name = n
  }

  var description: String{
    "\(name): \(club?.name ?? "帰宅部")所属"
  }

  deinit {
    print("\(name) : deinit")
  }
}

//Clubクラスは変更なし//

do {
  let hoge: Club = Club(n: "hoge部")
  let taro: Student = Student(n: "taro")
  hoge.add(taro)
  print(hoge)    // hoge部: 1人
  print(taro)    // taro: hoge部所属
}
// taro : deinit
// hoge部 : deinit
print("終わり")    // 終わり

例えばこのようにunwnedだけでインスタンスを参照すると、リファレンスカウンタを増やさないので、
即座にインスタンスが解放されて、その後変数を使うと落ちる。

unowned let taro: Student = Student(n: "taro")    // taro : deinit
print(taro)    //落ちる

オプショナルチェーン

例えばTeacherクラスを追加し、生徒クラスのクラブクラスの先生クラスの名前プロパティにアクセスするのには3回unwrapする必要がある。

class Student    //変更なし

class Club: CustomStringConvertible {
  let name: String
  var members = [Student]()
  weak var teacher: Teacher?    // 追加

  init(n: String){
    name = n
  }

  func add(_ s: Student){
    members.append(s)
    s.club = self
  }

  var description: String{
    "\(name): \(members.count)人"
  }

  deinit {
    print("\(name) : deinit")
  }
}

class Teacher {
  let name: String

  init(n: String){
    name = n
  }
}
let taro: Student? = nil
print(taro!.club!.teacher!.name)    // プロパティのどれかがnilならerror

これをif let で書くと記述が長くなってしまう。
しかしオプショナルチェーンを使えばコンパクトに書ける。

if let name = taro?.club?.teacher?.name {
  print(name)
}

この例ではtaroがnilではないかつclubがnilではないかつteacherがnilではないとき、中の処理が実行される。

オプショナルチェーンを使った代入

nilでなければ代入される。

do{
  let taro: Student?
  taro = Student(n: "taro")
  let hoge = Club(n: "hoge")
  hoge.add(taro!)
  let t = Teacher(n: "fuga")
  taro?.club?.teacher = t

  if let name = taro?.club?.teacher?.name {
    print(name)    // fuga
    print(type(of:name))
  }
}

nilなら代入されないのでif文が実行されない

do{
  let taro: Student?
  taro = Student(n: "taro")
  let hoge = Club(n: "hoge")
  // hoge.add(taro!)    // コメントアウトしてみる
  let t = Teacher(n: "fuga")
  taro?.club?.teacher = t

  //nilになるので実行されずに終わる
  if let name = taro?.club?.teacher?.name {
    print(name)
    print(type(of:name))
  }
}

キーパス

キーパス : インスタンスが相互に参照し合う関係があった時、あるインスタンスを起点として参照をたどって別のインスタンスを参照できる経路。

do{
  let taro: Student?
  taro = Student(n: "taro")
  let hoge = Club(n: "hoge")
  hoge.add(taro!)
  let t = Teacher(n: "fuga")
  taro?.club?.teacher = t

  let path = \Student.club?.teacher?.name    // これがキーパス

  if let name = taro![keyPath: path] {
    print(name)
    print(type(of:name))
  }
}

メモリアクセスの安全性

Swift処理系はメモリへのアクセスの安全性を高めるために、排他規則という原則に従ってコンパイル時に性的チェックを行っている。
具体的には3つの条件を満たす複数のアクセスがあった場合に問題が発生する。
1.少なくとも1つが書き込みアクセス
2.それらがメモリの同じ位置にアクセスする
3.それらの実行が重複する

メモリへのアクセスの可能性は、変数の参照、代入、inout引数を使った関数呼び出し、構造体のmutatingメソッド呼び出しがある。
inoutとmutatingは開始から終了までに別のコードが実行される可能性がある。

例としてはswap関数で同じ配列の中身を入れ替えようとするとエラーとする。

var list = [0, 1, 2, 3]
swap(&list[1], &list[2])    // error swapAtを使うように促される。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【iOS】デバイス(ローカル)にデータを保存する方法

iOSのアプリでは
重いリソース(大きいデータや画像など)を外部から毎回取得してくると
パフォーマンスや通信量に負担がかかってしまうということもあり
端末(ローカル)にデータを保存して
同じデータの場合は端末上のデータを利用することがあります。

そしてその中でも
データの種類や使用用途によって
保存方法や保存場所も変える必要があります。

これは
扱いやすさという点だけではなく
アプリ審査のリジェクトを防ぐという点でも
必要になってきます。

今回は
端末にデータを保存する方法にはどんなものがあるのか?
どうやってデータは保存されているのか?
どういうデータをどういう方法で保存する必要があるのか?

などについて見ていきたいと思います。

今回取り上げるのは下記の4つです。

  • UserDefaults
  • ディスク上のファイル
  • Keychain
  • Database

UserDefaults

アプリ内の
Library/Preferencesディレクトリに
plistファイルとしてデータを保存しています。

https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW1

データの読み書きは速いか?

ディスクへの書き込みが発生するため
それなりのコストはかかりますが
アプリ起動時にUserDefaultsはメモリ上に展開されるので
データの読み込みは速いです。

どういうデータを保存するか?

boolなどのプリミティブ型を使用して
アプリのユーザーの設定やユーザー体験を向上させるような
データを保存するのに向いています。

メモリに展開されるので
あまり大きなデータを保存してしまうと
端末メモリを圧迫してしまいます。

保存したデータはいつ削除されるか?

アプリが削除されると消えます。

注意点

UserDefaultsは値をそのまま保存しており
plistの中身を書き変えされてしまうリスクもあります。

そのため個人を特定できるようなセキュアな値を保存してはいけません。
(emailアドレスやパスワードなど)

使い方

UserDefaultsにはデフォルトのstandardという
staticなプロパティを利用することができます。

UserDefaults.standard.set(true, forKey: "isLoggedIn")
let isLoggedIn = UserDefaults.standard.bool(forKey: "isLoggedIn")

また
独自のUserDefaultsのインスタンスを生成することもできます。

let myUserDefaults = UserDefaults("suiteName: com.myapp.myUserDefaults")
myUserDefaults.set(true, forKey: "isLoggedIn")
let isLoggedIn = myUserDefaults.bool(forKey: "isLoggedIn")

詳細はドキュメントや多くの実装がありますので
そちらを参照してください。
https://developer.apple.com/documentation/foundation/userdefaults

WWDC2019では
Swift5.1から導入されたProperty Wrappersを利用して
簡単にアクセスできる例も紹介されていました。

https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md#user-defaults
https://developer.apple.com/videos/play/wwdc2019/402

ディスク上のファイル

アプリ内の特定のディレクトリへ
ファイルとしてデータを保存することができます。

データの読み書きは速いか?

UserDefaultsに比べると
扱っているデータも大きく
読み込みは遅くなる傾向にあります。

どういうデータを保存するか?

ユーザーが作成したテキストやPDF
ダウンロードした画像や動画
ネットワークから受け取ったレスポンス(JSONをエンコードしたもの)
などUserDefaulsよりも大きな容量のデータを保存します。

保存したデータはいつ削除されるか?

アプリが削除されるのと同時に削除されます。

注意点

UserDefaultsと同様に保存されるデータは自動で暗号化されません。

端末内から見られるリスクに加え
iCloudを経由して見られてしまう可能性があります。

そのため個人を特定できるようなセキュアな値を保存してはいけません。
(emailアドレスやパスワードなど)

また保存するディレクトリによって保存できる
データの種類が変わってきます。
これに違反するとアプリの審査でリジェクトされる可能性があります。

まずは下記のような種類があります。
大まかにまとめると下記のようになります。

ディレクトリ 保存内容 データの読み込み データの書き込み iTunes、iCloudへのバックアップ
Documents ユーザ作成のコンテンツ(文書や画像、動画など)
Documents/Inbox 他のアプリから受け取ったファイル ○(削除のみ)
Library ユーザ作成以外に保存したいファイル ○(Cache以外)
Library/Cache キャッシュでなど自動削除されてもよいファイル ×
tmp アプリ起動中など一時的に使用するファイル ×

https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html

さらにガイドラインには下記のように書かれています。


1.
ユーザが作成した文書やその他のデータ
アプリで再生成できないようなデータはDocumentsディレクトリに保存する。


2.
再ダウンロードや再生成可能なデータはLibrary/Cachesディレクトリに保存する。

例:
漫画や雑誌、マップアプリなどで使われるデータベースのキャッシュファイルなど


3.
一時的に保存が必要なものはtmpディレクトリに保存する。
不要になった際には削除をして端末の空きスペースを圧迫させないこと。


4.
もし特定のファイルで端末の空きスペースが少ない場合でも
削除されないようにしたい場合は
"do not back up"属性を設定すること。
これはどのディレクトリにのファイルでも有効になる。

https://developer.apple.com/documentation/foundation/urlresourcekey/1408756-isexcludedfrombackupkey

ただし空きスペースを使用し続けているため
監視を続けて定期的に削除すること。

例:
再生成できるけどアプリを正しく動作させるのに必要なものや
オフライン時でもユーザが使用できるようにしたいものなど。


https://developer.apple.com/icloud/documentation/data-storage/index.html

使い方

FileManagerを使用します。

do {
    let fileManager = FileManager.default
    let docs = try fileManager.url(for: .documentDirectory,
                                   in: .userDomainMask,
                                   appropriateFor: nil, create: false)
    let path = docs.appendingPathComponent("myFile.txt")
    let data = "Hello, world!".data(using: .utf8)!

    fileManager.createFile(atPath: path.absoluteString,
                           contents: data, attributes: nil)
} catch {
    print(error)
}

Keychain

データの読み書きは速いか?

パフォーマンスが良くないといった情報は見つかりませんでしたが
暗号化や復号することを考えるとUserDefaultsと比べて多少はコストが増えると考えています。
(もしそういう情報がありましたら教えて頂けましたらうれしいです??‍♂️)

どういうデータを保存するか?

データを暗号化できるため
emailやOAuthのトークンなどセキュアな小さい情報を
保存するのみ主に使用されます。

保存したデータはいつ削除されるか?

アプリを削除してもデータは残ります。
削除をするためには自身でAPIを呼び出して削除する必要があります。

let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { 
    throw KeychainError.unhandledError(status: status) 
}

https://developer.apple.com/documentation/security/keychain_services/keychain_items/updating_and_deleting_keychain_items

注意点

Keychainはプロビジョニングファイルと紐づけて暗号化されており
セキュリティは強固だとは思いますが
それでもと端末上にデータを保存するため
情報が見られてしまう可能性は考慮した方が良いかもしれません。

使い方

多くのKeychainのAPIはC言語ベースで書かれており
扱いづらい部分があります。

https://developer.apple.com/documentation/security/keychain_services

Appleはサンプルコードで利用方法を紹介していますが
かなり古いコードです。(Swift3)
https://developer.apple.com/library/archive/samplecode/GenericKeychain/Introduction/Intro.html#//apple_ref/doc/uid/DTS40007797-Intro-DontLinkElementID_2

自身で扱いやすいラッパークラスを生成したり
すでに便利なライブラリもたくさんありますので
そちらを活用するのが良いかもしれません。

ライブラリ例:
https://github.com/kishikawakatsumi/KeychainAccess

Databases

Core DataやRealm、SQLiteなど
特定のフォーマットやアクセス方法で
端末上のデータを扱います。


Core Dataに関しては正確にはDatabaseというご意見もあるようですが
同じように扱われるという点から例として挙げさせて頂いています。
https://davedelong.com/blog/2018/05/09/the-laws-of-core-data/

データの読み書きは速いか?

データはDocumentsディレクトリに保存されることが多く
ディスク上のファイルへの読み書きと同様に
UserDefaultsに比べると時間がかかります。

どういうデータを保存するか?

大きいデータや
将来的にデータの数がどんどん増えそうなデータを
配列として保存することに適しています。

特に
クエリを利用して
特定条件のデータを取得できたり
データ同士の関連性を持つことができるため
構造化されたデータが扱いやすくなっています。

例:
APIから取得したJSONデータを
独自のモデルにデコードしたものなど

保存したデータはいつ削除されるか?

アプリが削除されると消えます。

注意点

データはDocumentsディレクトリに
保存されることが多く
データが見られてしまう可能性があるため
セキュアな情報を保存することには向いていません。

またスレッドセーフでない場合もあるため
複数スレッドから同時にアクセスする場合は
自分でコントロールする必要があります。

使い方

基本的にはSQLでアクセスするところを
利用しやすいようにラップしたAPIが提供されています。
具体的な利用方法はDatabaseによって異なりますので
APIのドキュメントなどをご参照ください。

Core Data
https://developer.apple.com/documentation/coredata

Realm
https://realm.io/docs/swift/latest/api/

SQLite
https://github.com/stephencelis/SQLite.swift/blob/master/Documentation/Index.md#sqliteswift-documentation

まとめ

端末(ローカル)にデータを保存する方法を見ていきました。
それぞれ特徴によって
保存するべきデータの種類が変わってきたり
保存先も変わってきます。

特にユーザを特定できるような個人情報に関しては
情報漏洩など犯罪に関わってしまうリスクもあるため
細心の注意が必要になります。

近年ではJWTなどを利用することで
個人情報を直接保持するリスクを
減らしていけるので
そういった仕組みは活用していきたいですね?

何か間違いなどございましたら
ご指摘して頂けますとうれしいです??‍♂️

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

SwiftWebUIでWebアプリをつくろう!

はじめに

WWDC2019で発表され、大いに注目を集めるSwiftUIですが、そのSwiftUIでWebアプリを作れることをご存知でしょうか?

SwiftWebUIを使えばできます!
SwiftWebUIは、そのままですが、SwiftUIをWebでも使えるようにしようというプロジェクトです。

Screen Shot 2019-12-09 at 6.45.00.png

今回は簡単にTODOアプリを作っていきます。

Task App Demo.gif

参考リンク

Hello World

Xcodeを開き、New Project, macOS, Command Line Toolを選択します。
Screen Shot 2019-12-09 at 5.46.11.png

適当に名前を入力し、作成できたらSwift Package ManagerでSwiftWebUIを入れていきます。

[iOSアプリ開発にSwift Package Managerを使おう - Qiita]のリンクがわかりやすいです。

きちんと導入できれば、main.swift で下記をコピペしてBuild&Runして、http://localhost:1337にアクセスすれば、テキストの内容(Hello World)が表示されているでしょう

main.swift
import SwiftWebUI

SwiftWebUI.serve(Text("Hello World"))

続いて簡単なTODOアプリを作っていきましょう

TODOアプリ

モデルを定義します。

Task.swift
struct Task: Identifiable {
  var text: String
  var createdAt: Date
  var isFinished: Bool

  var id: String { text }
  var createdAtString: String {
    let f = DateFormatter()
    f.timeStyle = .short
    f.dateStyle = .short
    f.locale = Locale(identifier: "ja_JP")
    return f.string(from: createdAt)
  }
}

タスク一つ一つのUIを作成します。
Viewが一つのtaskを持っていて、Doneボタンを押せばisFinishedtrueになってタスクが完了し、文字色やViewのEnabledが制御されています。

TaskRow.swift
struct TaskRow: View {

  @State var task: Task

  var body: some View {
    HStack {
      Button("Done") {
        self.task.isFinished = true
      }

      VStack(alignment: .leading) {
        Text(task.name)
          .font(.headline)
          .color(task.isFinished ? .secondary : .primary)
        Text(task.createdAtString)
          .color(.secondary)
          .font(.subheadline)
      }
    }
    .padding()
    .disabled(task.isFinished)
  }
}

上記のUIは↓のようになります。

Screen Shot 2019-12-09 at 6.10.03.png

モデルとUIができればモックが作れるので、main.swiftを書き換えていきましょう。

import Foundation
import SwiftWebUI

struct MainView: View {

  @State private var tasks: [Task] = [
    Task(name: "Sample Task", createdAt: Date(), isFinished: false),
    Task(name: "Sample Task", createdAt: Date(), isFinished: false),
    Task(name: "Sample Task", createdAt: Date(), isFinished: false)
  ]

  var body: some View {
    Form {
      Section(header: Text("Task App").font(.title)) {
        List(tasks.identified(by: \.id)) { TaskRow(task: $0) }
      }
    }
  }
}

SwiftWebUI.serve(MainView())

この状態でBuild&Runすればタスクが画面の真ん中に表示されているかと思います。

Screen Shot 2019-12-09 at 6.13.09.png

あとはタスクを追加できるようにすれば完了です。

TaskInputViewを作成し、textをTextFieldで変更していき、追加ボタンを押したらactionを呼ぶようにします。

TaskInputView.swift
import SwiftWebUI

struct TaskInputView: View {

  @State private var text: String = ""

  let action: (String) -> Void

  var body: some View {
    HStack {
      TextField($text)
      Spacer()
      Button("追加", action: {
        self.action(self.text)
        self.text = ""
      })
        .disabled(text.isEmpty)
    }
  }
}

一方で、main.swiftではTaskInputViewのactionでString型受け取るので、tasksの配列に追加していくようにしておきます。

main.swift
  var body: some View {
    Form {
      Section(header: Text("Task App").font(.title)) {
        TaskInputView(action: { text in
          let task = Task(text: text, createdAt: Date(), isFinished: false)
          self.tasks.append(task)
        })
        Spacer(minLength: 12)
        Text("Task List")
          .font(.title)
          .bold()
        List(tasks.identified(by: \.id)) { TaskRow(task: $0) }
      }
    }
  }

下記のようなアプリが出来上がります。

Task App Demo.gif

既知のバグとして

  • TextField内のtextの変更を受け取れていない
  • Task Listを空配列で定義するとタスクを追加できない

が残っておりいろいろ雑ですが、現状のSwiftWebUIの仕様など、詳しくみれていないので時間があれば調査したいと思います。

完成形のプロジェクトをGitHubに上げておきます。
GitHub - mtfum/SwiftWebUISample

懸念点

  • NavigationViewが利用できない
  • Buttonの色が変更されない
  • タスクの空配列に追加してもListに反映されない
    • TypeError: Cannot read property 'parentNode' of Nullのエラーが表示される

など、実際に触ってみるとバグも多く、利用していくにはまだまだ先になりそうです。
また、ブラウザ上でSwiftを動かすためにはSwift Wasmの対応も待たなければなりません。

最後に

SwiftWebUIはプロダクション用ではなくSwiftUIを学ぶのに適していると注意書きされています。

Disclaimer: This is a toy project! Do not use for production. Use it to learn more about SwiftUI and its inner workings.

しかし、SwiftでWebアプリがつくれる日が近づいている証でもあり、まだまだ先は長そうですが今からとても楽しみです。

SwiftUIを触ってみるには良い環境かもしれないので、興味のある人は触ってみてください。
ということでSwiftWebUIの紹介を示させてもらえればと思います。
最後までお読みいただきありがとうございました。

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

iOS開発完全に理解したのその一歩先へ...

この記事は CBcloud Advent Calendar 2019 4日目の記事です。
おそらくiOS/Androidエンジニアとして、CBcloudに2019/5/1から参画しましたが、
なぜか今はPM業のようなことをやっているentakuです。(この話は後日書こうとおもいます。)
念のため言っておくと、今の働き方に満足しているので、その辺は問題ないのです。

0. 自己紹介

entakuです。経歴は下記のようになっています。
なんちゃってSE
->NWエンジニア[SIer]
->
サーバーサイドエンジニア[SIer] ・java/PHP..etc
->
iOS/Android アプリ開発[スタートアップ]
->
Vue / Android アプリ開発 @ CBCloud

 最近は「ユーザーを動かす開発者」でありたいなと思います。

直近ではiOS開発が好きで下記のような活動をしていました。

一人iOSエンジニアでも気持ちよく開発できるiOSアプリ開発者になる!
iosの開発を始めたあの日の僕に伝えたいこと。

このQiitaでは「iOS開発完全に理解した」から、どのようにしてその先

1. 設計の知識を深める

iOS開発を行っていると、こういうときどう書けばいいんだ..という様々な場面に巡り会います。
これは公式で指標となりうる設計フレームワークが無いこと、また

僕は「iOSアプリ設計パターン入門」という本をiOSエンジニアの友人から勧められ購入しました。

特に第1章「設計するということ」今後のiOS開発において不変的な考え方が述べられていると思います 
また、設計をただ紹介するだけで無く、

  • 今までのやり方のどんな問題があり
  • その問題はなぜ起こっていて
  • どのようなアプローチで解決できるのか?

がまとめられている、iOS開発に欠かせない内容だと思います。

また

  • 今までのやり方のどんな問題があり の箇所は、 一度iOS開発をした方は共感する部分が多いと思いますし、一歩先に進むためにはよいものだと思っています!

詳細な内容はぜひ購入して読んでいただきたいです!

※peaksリンク iOSアプリ設計パターン入門

2. RxSwiftへの挑戦

なぜRxSwiftを使うのか?

正直僕自身もRxSwiftの名前に先行して利用していた部分がありますので、僕の理解の範囲で書きます

基本的にRxSwiftでは下記の2つの処理をViewModelで繋げます。

- ユーザーの動作(input)
- APIなどデータを取得する処理(output)

下記の例では僕が書いたViewModelの簡単な例を載せております。

「viewDidLoad」がViewControllerから呼び出された際に、「viewDidLoadSubject」内の処理が呼び出されます。
ViewControllerでは「teams」を監視した処理を「viewModel.teams」内に書いており、何を検知した際にどのような処理を行うのか?をわかりやすく分離してあります。

class SportTalkListViewModel {

    override func viewDidLoad() {
        super.viewDidLoad()


        viewModel.viewDidLoad.onNext(())
        bindViewModel()
    }
    func bindViewModel() {


        viewModel.teams
            .bind(to: tableView.rx.items(cellIdentifier: "SportTalkListCell", cellType: SportTalkListCell.self)) { _, element, cell in
                cell.team = element
            }.disposed(by: disposeBag)

    }

class SportTalkListViewModel {

    private let disposeBag = DisposeBag()

    // input property
    var viewDidLoad: AnyObserver<Void>


    // output property
    var teams: Observable<[Team]>


    init(provider: TeamsProviderProvider = TeamsProviderProvider()) {
        let teamsRelay:BehaviorRelay = BehaviorRelay<[Team]>(value: [])
        let viewDidLoadSubject = PublishSubject<Void>()

        teams = teamsRelay.asObservable()
        viewDidLoad = viewDidLoadSubject.asObserver()


        viewDidLoadSubject.asObservable()
            .flatMap{ _ in provider.getTeams() }
            .flatMap { teams -> Observable<[Team]> in

                return .just(teams.teams)
            }.bind(to: teamsRelay)
            .disposed(by: disposeBag)


    }


正直このようなシンプルな構成では効力を発揮しませんが、画面内で多くの「ユーザーの動作」と「APIなどデータを取得する処理」が存在する場合、処理の変更や追加がしやすいです。

どのようにしてRxSwiftを学んだか??

これは強く断言できますが、
RxSwift経験者にマンツーマンで教えてもらうことがもっとも良い方法です!
僕自身も経験者に聴きながら、書きながら覚えていきました。
RxSwiftは自分でパーツを作りながら、少しずつできるようになっていくようなイメージで取り組んでいます。
この自分のパーツを増やしていく感覚が大切かなと考えています。

とても拙いアプリではありますが、下記でRxSwiftを利用した簡単すぎるアプリを作りました
footballアプリ(GitHub)

3. SwiftUIへの挑戦

ついについにiOSにも宣言的UIの潮流がきました!
正直AndroidのXMLで書けるUIに長らく嫉妬していましたね...

1.HotReloadでUIがリアルタイムかつ複数パターン確認できるようになったこと
2.TableViewなど固有Viewではなくなったところ

下記はいままでTableViewCellで表されていたものの一例です。
HStack (X軸を基準として横並びにViewを並べるレイアウト)を使って比較的自由なレイアウトができます。

struct TeamRow: View {

    var team: Team

    var body: some View {
        HStack {
            team.image
                .resizable()
                .frame(width: 50, height: 50)

            Text(team.name)

            Spacer()
        }
    }
}

struct TeamRow_Previews: PreviewProvider {
    static var previews: some View {

        Group {
            TeamRow(team: sampleTeams[0])
            TeamRow(team: sampleTeams[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}

SwiftUI公式
SwiftUIでつくったもの

4. まとめ

今年は多くの時間をiOS開発に使えたとは言い難いですが、自分なりにできる範囲でiOS開発を楽しみました?

多くの時間をiOSアプリ開発に注力できなかったとしても、一歩先へ行けるように来年も楽しんでアプリ開発します

この記事がどこかのiOSアプリ開発者の新しい一歩に役に立つことができたらとてもうれしいです!
お気軽にご意見やコメントください!

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

プロパティやメソッドの並び順テンプレート(Swift)

はじめに

SwiftLintには、クラスやプロパティなどの並び順を強制するルールがあります。

ルール名 概要
file_types_order ファイル内の型の並び順を強制する
type_contents_order 型内のプロパティやメソッドの並び順を強制する

これらのルールを適用することで、必然的にコードが整理されて可読性が上がります。

しかし、並び順を覚えるのは大変です。
ぱっと見でどこに何を書けば(書いてあるか)わかるよう、テンプレートを作成しました。

ルールの例からほぼ変えていないので、公式を見るだけでもいいと思います。
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#file-types-order
https://github.com/realm/SwiftLint/blob/0.38.0/Rules.md#type-contents-order

自分ルール

同じグループの場合、アクセス修飾子の大きいものから順に書きます。
( openpublicinternalfileprivateprivate )

アクセス修飾子も同じ場合、読み下しやすい順に書きます。

class TestViewController: UIViewController {

    // MARK: Other Methods

    // privateよりinternalメソッドから先に書く
    func hoge() {}

    // 呼び出し側から先に書くと読み下しやすい
    private func foo() {
        bar()
    }

    private func bar() { }
}

準拠したプロトコルのメソッドは、 // MARK: {プロトコル名} としてOther Methodsの上に書きます。

// MARK: Protocols

protocol TestViewControllerDelegate: AnyObject {
    func didPressTrackedButton()
}

// MARK: Main Type

class TestViewController: UIViewController, TestViewControllerDelegate {
    // MARK: TestViewControllerDelegate

    func didPressTrackedButton() {
        // some code
    }

    // MARK: Other Methods

}

環境

  • SwiftLint:0.38.0

テンプレート

// MARK: Protocols

protocol TestViewControllerDelegate: AnyObject {
    func didPressTrackedButton()
}

// MARK: Main Type

class TestViewController: UIViewController, TestViewControllerDelegate {
    // MARK: Type Aliases

    // MARK: Classes

    // MARK: Structs

    // MARK: Enums

    // MARK: Stored Type Properties

    // MARK: Stored Instance Properties

    // MARK: Computed Instance Properties

    // MARK: IBOutlets

    // MARK: Initializers

    // MARK: Type Methods

    // MARK: View Life-Cycle Methods

    // MARK: IBActions

    // MARK: TestViewControllerDelegate

    // MARK: Other Methods

    // MARK: Subscripts

}

// MARK: Extensions

extension TestViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
}

おわりに

Xcode上で新しくファイルを追加したら、まず上記のコメントをコピペするのがオススメです。
コーディング後、未実装のグループのコメントを削除すると見通しがよくなります。

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

SwiftUI 導入で LLVM のドキュメント読む羽目になった話

はじめに

この記事は CyberAgent Developers Advent Calendar 2019 9日目の記事になります。
今回は、コンパイラ周りの話も入ってくるので、日頃そこら辺に触れない方でも読んでいただけるようにスライドを多用した記事にしてみました。最後まで読んでいただけると嬉しいです!

SwiftUI 導入してます!

xcode-previews-and-llvm.005.png
弊社が運用する恋活サービス「タップル誕生」iOS 版では SwiftUI を一部で導入しています。
ただ、導入は簡単ではありませんでした。タイトルの通り、 LLVM のドキュメントを読む羽目になった経緯と問題の解決方法を紹介したいと思います。手っ取り早く 解決方法のみを知りたい! という方は、ページの一番下までスクロールしてみてください!

SwiftUI 導入方法

xcode-previews-and-llvm.007.png
SwiftUI と UIKit には互換性があるため、共存させることが可能です。なので、 UIKit で設計されたものを SwiftUI のようにプレビューさせることも可能になります。ここらへんに関しては、以前記事を書いてますので参考にしてみてください。

iOS12 以下をサポートするプロダクトで SwiftUI の恩恵を得る方法
参考:AkkeyLab/AutoPreviewable
参考:AkkeyLab/XcodePreviewsTemplate

地獄の始まり

xcode-previews-and-llvm.010.png
サンプルプロジェクトを作って SwiftUI を試していた私は完全に油断していました。実際のプロダクトで Xcode Preview が動かないんです(※)。しかも、1行の長々とした不親切なエラーを出力するだけ(内容的には、「〜が読み込めませんでした」という感じでした)。

※ SwiftUI を用いた設計・実行等は可能です

xcode-previews-and-llvm.019.png
先程のエラーの一部で検索を試みますが、 Flutter や Xamarin の記事ばかりで肝心の SwiftUI に関連するのもにはたどり着けませんでした。
この情報不足感は Swift 発表当初を思い出しますね。だからといって、諦めるわけにはいかないのですが、日頃の業務の空き時間等にちょこちょこ SwiftUI への対応を進めていたので、一旦ここで調査を中断しました。

xcode-previews-and-llvm.023.png
そして、休日に個人で開発を行っている動画視聴アプリ「AkkeyTV」の SwiftUI 対応を試みました。なんとなくわかっていましたが、同じく Xcode Preview に失敗しました。(スクショは、リポジトリの issue に貼り付けたエラーコード)
ただ、このエラーコードが問題解決に大きく貢献することになります。

xcode-previews-and-llvm.028.png
先程のエラーをもとに調査を進めたところ、 SwiftUI で同様の問題に直面している記事にたどり着きました。そこに書かれていたことをざっくりとまとめると「カバレッジを有効にした状態でプレビューできない」というものでした。
実際にカバレッジを無効にしてみると tapple でも AkkeyTV でも Xcode Preview が正常に動作するようになりました!しかし、テストを書きながら開発を進めているため、この解決方法はボツにしました。

引用元:Undefined symbols ___llvm_profile_runtime

xcode-previews-and-llvm.032.png
更に調査を進めたところ、 SwiftUI 以外でも同様のエラーが出ることがあり、それは static framework に関係しているということがわかりました。そして、 Linker flags に -fprofile-instr-generate を追加するとうまく動作するという解決策にたどり着いたのです!
実際にフラグを追加してみると tapple でも AkkeyTV でも Xcode Preview が正常に動作するようになりました!

引用元:ReactiveCocoa/ReactiveSwift > issue > Optimization Level configuration #552

状況整理

xcode-previews-and-llvm.044.png
ここで一旦、状況を整理します。
問題の解決はできたものの、「 -fprofile-instr-generate というフラグが一体何者なのか」という新たな疑問が生まれてきました。このフラグが原因でアプリが正常に動作しなくなったり、開発効率が低下するようなことがあってはいけないため、このフラグに対する調査も引き続き行っていきます。

問題解決の裏側

xcode-previews-and-llvm.047.png
まず、「このフラグが何者なのか」この疑問に対する答えがこれです。 clang を使ってソースコードのカバレッジも出力したいときに付加させるフラグだということがわかります。

xcode-previews-and-llvm.051.png
さて、先程の説明文に出てきた見慣れない用語についても触れておきましょう。
これで、 -fprofile-instr-generate というフラグに関してはある程度わかったと思います。

xcode-previews-and-llvm.055.png
ここで、少し前のスライドにフラグに関することも追加してみます。この解釈だと、矛盾が生じているように見えます。
そこで、もう少し視野を広げて、 Xcode のビルド方法について見ていくことにします。

xcode-previews-and-llvm.058.png
iOS は Objective-C と Swift を1つのプロジェクトで利用できることからもわかるように C 系のコンパイラである clang と Swift コンパイラである swiftc によってビルドが行われています。
ここで、 Linker については こちら を参考にしてください。

内部的に Xcode は各コンパイラに対して引数を渡してコマンドを叩いているだけなので、そのコマンド引数に渡されるオプションについて調べてみることにします。

xcode-previews-and-llvm.060.png
プロジェクトの設定ファイルなどに記載された各種設定項目から、コマンド引数に渡すオプションに変換するルールブックのようなものが Xcode の奥深くに置かれています。
これは clang の場合になりますが、 xcspec という拡張子のファイルに記述されており、カバレッジが有効であるときに2つのフラグをコマンド引数に設定するように記述されていることが確認できます。つまり、カバレッジを有効にした時点で -fprofile-instr-generate というファグはコマンド引数に渡されていたということです。

では、 Linker flags に -fprofile-instr-generate を追加することと、カバレッジを有効にすることにはどのような違いがあるのでしょうか。→【Xcode Build System】

xcode-previews-and-llvm.064.png
swiftc に対する xcspec も同様の処理が記述されています。

xcode-previews-and-llvm.065.png
同様のファイルは Linker に対しても存在していますが、カバレッジに関する処理はありませんでした。

xcode-previews-and-llvm.068.png
ここで、もう少し深く見てみようと思います。
我々が記述するソースコードが機械語に変換されてアプリとして起動するまでを図にまとめてみました。

xcode-previews-and-llvm.071.png
各種コンパイラと Linker の存在を図に挿入し、渡されるフラグに関しても追記してみました。
ここでわかってくるのは、カバレッジを有効にすることでフラグが渡される対象と、 Linker flags のフラグが渡される対象が異なっているということです。

考察

xcode-previews-and-llvm.075.png
今後、引き続き調査と勉強を継続していきます。
そのため、この記事には解釈の間違い等が含まれている可能性があります。間違いにお気づきの際は、コメント等いただけると嬉しいです。

さいごに

タイトルに「読む羽目になった」と書きましたが、実際に原因を探り、コンパイラ等の技術について学ぶ時間はとても有意義な時間でした。(記事を読んで伝わったのではないかとは思いますが)嫌々ドキュメントを読んでいたわけではないのでご安心ください!

最後までご覧いただき、ありがとうございました!

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

UIButtonをaddSubViewする時はsuperviewを適切に選択すべし(戒め)

ある日のこと……

画像の上にボタンを置いて、ボタンタップ時に処理を追加しようとした。

作りたいもの

完成図

Simulator Screen Shot - iPhone 11 Pro Max - 2019-12-08 at 20.50.46.png

早速作ってみる

コードを書いた

これで完了!……と思うじゃん?

ViewController.swift
import UIKit

final class ViewController: UIViewController {
    private lazy var imageView: UIImageView = {
        let imageView = UIImageView()
        // 本当は画像を置いていたのだが、サンプル用のコードなのでbackgroundColorで妥協
        imageView.backgroundColor = UIColor.systemPink.withAlphaComponent(0.5)
        imageView.layer.cornerRadius = 16
        imageView.clipsToBounds = true
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()

    private lazy var button: UIButton = {
        let button = UIButton()
        button.setTitle("Please tap!!", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 24)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        return button
    }()

    private func layoutImageView() {
        view.addSubview(imageView)
        NSLayoutConstraint.activate([
            imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            imageView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
            imageView.heightAnchor.constraint(equalTo: view.widthAnchor, multiplier: 9 / 16),
        ])
    }

    private func layoutButton() {
        imageView.addSubview(button)
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }

    @objc
    private func buttonTapped() {
        print("button tapped!!")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        layoutImageView()
        layoutButton()
    }
}

しかし…

⌘+Rを押してビルドされるのを待ち、いざ、ボタンをタップ!!
……あれ?コンソールに何も出てこないぞ??どうやらボタンがタップできていないらしい。
何が悪いんだ?

原因究明

UIViewには、isUserInteractionEnabledというプロパティがある

isUserInteractionEnabledはタップなどのイベントを有効にするかどうかのプロパティである。
で、このデフォルト値はtrueであったはず。なんたって、特にUIButtonはタップイベントを検知するためのUIパーツだし。

念のため、ドキュメントを確認しよう

isUserInteractionEnabled - UIView | Apple Developer Documentation

The default value of this property is true.

ですよねー。
……と思っていたら、下の項目にこんなことが。

Some UIKit subclasses override this property and return a different default value. See the documentation for that class to determine if it returns a different value.

なるほど。UIKitのサブクラスの中にはデフォルト値がtrueではないものがあると。

念のため、UIButtonのドキュメントも見てみましょう。
UIButton - UIKit | Apple Developer Documentation
当然ですが、isUserInteractionEnabledに関する項目はない。
うーん……?

DeveloperサイトをisUserInteractionEnabledでググってみた

スクリーンショット 2019-12-09 0.06.53.png

そういえば、UIButtonのsuperViewをUIImageViewにしたぞ……?
superViewをUIViewに変更して再度ビルドして試してみる。

-        imageView.addSubview(button)
+        view.addSubview(button)

今度はちゃんとタップが検出され、コンソールにbutton tapped!!と表示された!

何故ボタンのタップが検出されなかったか

理由が明記された文章は探した限りだと見つかりませんでした」
ただ、UIImageViewのsubViewだったUIButtonのタップが検出されなかったのに、UIViewのsubViewであったUIButtonのタップが検出された事実を考えれば、あるviewのisUserInteractionEnabledがfalseの場合、そのsubviewのisUserInteractionEnabledがtrueであってもタップは検出されなくなってしまうようです。

解決策

画像の上にボタンを置きたくなることはあると思います。その場合、簡単な対処法としては

  1. UIImageViewのisUserInteractionEnabledをtrueに変更する
  2. UIButtonのsuperViewをUIImageViewではないものにする

の2つが考えられます。どちらを選択するかは、Viewの構造によって適切に選択すると良いと思います。
上記の場合は、viewをsuperviewにする後者よりは、UIImageViewをsuperviewとする前者の方がよい気がします。
ちなみに今回私がハマったのは、UICollectionViewCell上に置いたUIImageViewとその上のUIButtonだったため、後者を選択しCellをsuperviewとしました。

Debug View Hierarchyだとどうしてもbuttonが最も手前に見えるので一瞬しっくりこないのですが、まあsubviewなのだからそういうものなのかもしれませんね?

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

インデントってなんだ?

おい俺!コードが見にくいぞ!

swiftを今は初学者向けの本で学習をしているのだが、どうもサンプルコードを自分で書くと、コードが読みづらい。
というのも、ページの都合上改行しないといけない文がある為、「これが正しいコード」として認識をしていたのである。

例えば例として、こんなコードを書く。

import UIKit

class ViewController: UIViewController {
//スーパークラスUIViewControllerを継承しているclass ViewController

   @IBOutlet weak var label: UILabel!

    @IBAction func sayHello(_ sender: Any) {
    label.text = "こんにちは"
//
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }


}

でね、このコードのデコボコ具合ってなんか謎な訳ですよ。
初学者には。

コードを書くと意味分からないところで改行されるし。

だから見やすいコードっていうのは

import UIKit

class ViewController: UIViewController {
//スーパークラスUIViewControllerを継承しているclass ViewController

@IBOutlet weak var label: UILabel!

@IBAction func sayHello(_ sender: Any) {
label.text = "こんにちは"
//
}

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}


}

っていうもんだと思う訳ですよ。

整理されてるやん!って。

で、コードにエラーが出て知り合いのエンジニアさんにコードを送ると

「インデントのズレを直したいね」と言われた。

インデントのズレ?

インデントってなんだ

ウィキペディアによると

字下げスタイルまたはインデントスタイル(英: Indent style)とは、プログラミングにおいてプログラムの構造を明らかにするために、コードのブロックの字下げをどうするかを決めたものである。本項ではC言語やそれに類似した言語を主に扱うが、他のプログラミング言語(特に括弧を使用してブロックを記述する言語)にも適用可能である。字下げスタイルはプログラミング作法の一部である。

とある。

いや、下げる意味なんてないやん!
いいえ、おおありでした。

インデントは国語でいうところの段落!?

「今日は晴れでした楽しかったです明日も晴れるといいな。」

という文章があるとしよう。
この文章は果たして読みやすいだろうか。

「今日は、晴れでした。楽しかったです。明日も晴れるといいな。」なら読みやすい。

つまり先ほどの

import UIKit

class ViewController: UIViewController {
//スーパークラスUIViewControllerを継承しているclass ViewController

@IBOutlet weak var label: UILabel!

@IBAction func sayHello(_ sender: Any) {
label.text = "こんにちは"
//
}

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}


}

は、「今日は晴れでした楽しかったです明日も晴れるといいな。」という文章状態。
どこが段落で「。」があるのか分からない状態である

一方で

import UIKit

class ViewController: UIViewController {
//スーパークラスUIViewControllerを継承しているclass ViewController

   @IBOutlet weak var label: UILabel!

    @IBAction func sayHello(_ sender: Any) {
    label.text = "こんにちは"
//
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }


}

だと

import UIKit

class ViewController: UIViewController { ←段落1
//スーパークラスUIViewControllerを継承しているclass ViewController

   @IBOutlet weak var label: UILabel!

    @IBAction func sayHello(_ sender: Any) { ←段落2
    label.text = "こんにちは"
//
    } ←段落2の終わり

    override func viewDidLoad() { ←段落3
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    } ←段落3の終わり


}←段落1の終わり

となっている。

確かに、段落1の始まりと終わりの文字の銭湯が同じ位置にあるし、段落2と3の始まりと終わりも同じ位置にある。

これが全部同じ位置に配置されていると、どれ段落の始まりで終わりかが分からない。

今までなんとなくモッコリさせればいいと思っていたけど、これでスッキリ!!

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