Iganinのブログ

日頃の開発で学んだ知見を中心に記事を書いています。

【Swift/Kotlin】SwiftのswitchとKotlinのwhenの対比

はじめに

条件分岐を実装する際にif else以外に Swiftではswitch、 Kotlinではwhenを使用します。 基本的にはiOS開発を行っていますので、Kotlinのwhen文を書く際に書き方を思い出すのに少し時間がかかることがあり、 学習の意味もかねて両者の比較を行います。

環境設定

以下の環境を使用しています。どちらもWeb上のPlaygroundです。

比較

Kotlinの条件分岐で使用されるwhenでは、 when (対象の値)の後に 条件 -> 式で分岐後の挙動を記載します。 デフォルトの条件(記載されている条件全てに当てはまらなかった場合)は else で記載します。 また、 Kotlinでは when式を変数に格納できることも特徴です。

enum class SampleValue {
    a,
    b,
    c,
    d
}

fun main() {
    var string = "sample"
    when (string) {
        "sample" -> {
            println("this is sample")
        }
        "hoge" -> {
            println("this is hoge")
        }
        "huga" -> {
            println("this is huga")
        }
        else -> {
            println("other")
        }
    }
    
    val constant = when (string) {
        "sample" -> "sample"
        else -> "others"
    }
    
    val sampleValue = SampleValue.a
    
    when (sampleValue) {
        SampleValue.a, SampleValue.b -> println("a or b")
        SampleValue.c -> println("c")
        SampleValue.d -> println("d")
    }
}

Swiftのswitchでは switch 対象の値の後に case 条件: 式の形で分岐後の挙動を記載します。 デフォルトの条件(記載されている条件全てに当てはまらなかった場合)は default: 式 で記載します。 また、Swiftではswitchをそのまま変数に代入することはできません。 似たことを行いたい場合はクロージャの返却値として代入する必要があります。

import Foundation

let sample = "sample"
let a:String = {
    switch sample {
        case "sample": return "sample"
        default: return "other"
    }
}()

switch sample {
case "sample": print("this is sample")
case "hoge": print("this is hoge")
case "huga": print("this is huga")
default: print("this is other")
}

enum Value:Int {
    case a = 0
    case b
    case c
    case d
}

let valueSample:Value = .a

switch valueSample {
    case .a, .b: print("a or b")
    case .c: print("c")
    case .d: print("d") 
}

8/29追記

Twitterにてアドバイスいただいたので追記します。 KotlinのwhenではSmart Castが効くようです。 具体的には下記のようなコードを書いたときに、is Intのように型の一致で式を実行する際、 式に入ったときにはすでにxはInt型にCastされているため、型キャストの必要がなくInt型として扱うことができます。

when (x) {
  is Int -> Do Something ( この段階で xは Int型として認識されているので x + 1 = 2となる)
  is String -> Do Something(この段階でStringとして認識されているため、たとえばx + 1 とすると 11のように出力される)
  else -> Do Something
}

[毎日Kotlin] Day9.Smart casts(スマートキャスト) | DevelopersIO

【iOS】UINavigationControllerでcompletionHandlerを伴ったpush/pop遷移ができるようにする

はじめに

画面のモーダル遷移を実現するUIViewControllerpresent(viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?)dismiss(animated: Bool, completion: (() -> Void)?) では completionがあり画面遷移完了後の処理を記述することができます。

一方でUINavigationControllerpushViewController(viewController: UIViewController, animated: Bool)popViewController(animated: Bool)ではcompletionがなくデフォルトでは、 画面遷移完了後の処理を記述することができません。本記事ではUINavigationControllerでもdismissメソッドと同様に 画面遷移完了後の処理を記述する方法を記載します。

環境設定

以下の環境を使用しています。

  • Xcode10.2.1
  • Swift 5.0.1

方法

UINavigationControllerに以下のメソッドを追加します。

transitionCoordinatorViewControllerの遷移に関するアニメーションを規定するProtocolで、 遷移実行時にUIViewControllertransitionCoordinatorプロパティに格納されます。

transitionCoordinatoranimate(alongsideTransition:, completion:)メソッドのcompletiontransitionの完了後に呼ばれるため、ここに実行したいメソッドを入れれば画面遷移完了後に メソッドを呼ぶという目的を達成できます。

// TransitionCoordinatorはViewControllerのtransitionに関するアニメーションを規定するProtocolです
// transitionCoordinatorはactiveなtransitionやpresentation/dismissalが実行されている時に
// viewControllerのtransitionCoordinatorに含まれ、transitionが完了したタイミングで解放されます
extension UINavigationController {
    
    func popViewController(animated: Bool, completion: @escaping (() -> Void)) {
        popViewController(animated: animated)        
        if animated, let coordinator = transitionCoordinator {
            coordinator.animate(alongsideTransition: nil) { _ in
                // coordinatorで実行するanimationの完了時にcompletionが実行されます
                // 本メソッドにおいてcontextが同じanimationはpopViewControllerのため
                // pop完了後に本メソッドが実行されます
                completion()
            }
        } else {
            completion()
        }
    }
    
    func pushViewController(_ viewController: UIViewController, animated: Bool, completion: @escaping (() -> Void)) {
        pushViewController(viewController, animated: animated)
        if animated, let coordinator = transitionCoordinator {
            coordinator.animate(alongsideTransition: nil) { _ in
                // coordinatorで実行するanimationの完了時にcompletionが実行されます
                // 本メソッドにおいてcontextが同じanimationはpushViewControllerのため
                // push完了後に本メソッドが実行されます
                completion()
            }
        } else {
            completion()
        }
    }
}

ログの出力

以下のメソッドでログ出力を行い呼ばれる順番を確認しました。

// 呼び出し元
viewWillDisappear
viewDidDisappear
viewWillAppear
viewDidAppear

// 呼び出し先
viewWillDisappear
viewDidDisappear
viewWillAppear
viewDidAppear

// Completion
// UIViewController present および dismiss
// UINavigationController push および pop

結果は以下のようになりました。Aが呼び出し元のViewController、Bが遷移先のViewControllerです。 なおanimatedはtrueであってもfalseであっても同様の結果になりました。

// UIViewControllerのpresentおよびdismiss
A viewWillDisappear
B viewWillAppear
B viewDidAppear
A viewDidDisappear
present completion
B viewWillDisappear
A viewWillAppear
A viewDidAppear
B viewDidDisappear
dismiss completion

// 本記事でのUINavigationControllerのpushおよびpop
A viewWillDisappear
B viewWillAppear
A viewDidDisappear
B viewDidAppear
push completion
B viewWillDisappear
A viewWillAppear
B viewDidDisappear
A viewDidAppear
pop completion

modalでの遷移かpushでの遷移かの違いにより、viewWillAppearやviewDidAppearなどライフサイクル部分の順序に違いはありますが、 completionの呼ばれるタイミングは遷移元および遷移先のライフサイクルが全て完了した後であり、 presentメソッドやdismissメソッドのcompletionで実行しようとしていることをするには問題ないであろうことがわかります。

なお、補足ですが、たまにみられる以下の書き方ですと、completionのタイミングがvewDidDisappearやveiwDidAppearの前となってしまい、 presentやdismissとタイミングが大きく異なるため意図した挙動にならない可能性が高いです。

extension UINavigationController {
    
    func popViewController(animated: Bool, completion: (() -> Void)? = nil) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        popViewController(animated: animated)
        CATransaction.commit()
    }
    
    func pushViewController(_ viewController: UIViewController, animated: Bool, completion: (() -> Void)? = nil) {
        CATransaction.begin()
        CATransaction.setCompletionBlock(completion)
        pushViewController(viewController, animated: animated)
        CATransaction.commit()
    }
}

// ログの結果
A viewWillDisappear
B viewWillAppear
push completion
A viewDidDisappear
B viewDidAppear
B viewWillDisappear
A viewWillAppear
pop completion
B viewDidDisappear
A viewDidAppear

参考

【iOS】Place Holder(プレースホルダー)付きのUITextViewを作成する

はじめに

複数行のユーザーインプットを受け付けたいとき、UITextViewをよく使用します。 ユーザーがどのような内容を入力すれば良いかのヒントを与えるためにプレースホルダーの使用を検討することもよくあることだと思います。

ところが、UITextViewにはプレースホルダーが存在しません。したがって、UITextFieldのように placeholder にStringを入れればよい、というように簡単にはいきません。 本記事ではプレースホルダー付きのUITextViewの作成方法を記載します。

環境設定

以下の環境を使用しています。

  • Xcode10.2.1
  • Swift 5.0.1

方法

どういう風に実現するのが良いかと考えましたが、最終的にはUITextViewの上にUILabelを置くことにしました。 以下、作成したカスタムクラスのコードを記載します。

概要としては、UILabelをtextContainerのinsetやlineFragmentPaddingを考慮しながら配置、 編集開始と編集終了のイベントをKVOで受け取り、必要に応じてプレースホルダーの表示・非表示を制御しています。 プレースホルダーの文字列はプレースホルダープロパティに設定するのみで表示されるように作成しています。

import Foundation
import UIKit

@IBDesignable
public final class PlaceHolderTextView: UITextView {
    
    // MARK: - IBInspectable
    
    @IBInspectable
    public var cornerRadius: CGFloat {
        set { layer.cornerRadius = newValue }
        get { return layer.cornerRadius }
    }
    
    @IBInspectable
    public var borderColor: UIColor? {
        set { layer.borderColor = newValue?.cgColor }
        get {
            if let cgColor = layer.borderColor {
                return UIColor(cgColor: cgColor)
            } else {
                return nil
            }
        }
    }
    
    @IBInspectable
    public var borderWidth: CGFloat {
        set { layer.borderWidth = newValue }
        get { return layer.borderWidth }
    }
    
    /// プレースホルダー表示用の文字列
    /// 本プロパティの値がプレースホルダーとして表示される
    @IBInspectable
    public var placeHolder: String? {
        didSet {
            placeHolderLabel.text = placeHolder
        }
    }
    
    /// 本来持っているtextプロパティを上書きして、didSetをつけている
    /// このようにすることで、UITextViewの初期値に
    /// textが入っていた際にプレースホルダーを非表示にできる
    public override var text: String! {
        didSet {
            placeHolderLabel.isHidden = !text.isEmpty
            placeHolderLabel.sizeToFit()
        }
    }
    
    private let placeHolderLabel = UILabel(frame: .zero)
    
    // MARK: - Life Cycle
    
    public override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        commonInit()
    }
        
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    override public func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        commonInit()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UITextView.textDidEndEditingNotification, object: nil)
    }
}

private extension PlaceHolderTextView {
    
    private func commonInit() {
        
        // PlaceHolder表示用のラベルをUITextViewに加えConstraintをつける
        layer.masksToBounds = true
        addSubview(placeHolderLabel)
        
        // TextViewのtextと位置が同じになるように、
        // textContainerInsetとlineFragmentPaddingを考慮してConstraintをかける
        let padding = textContainer.lineFragmentPadding
        placeHolderLabel.translatesAutoresizingMaskIntoConstraints = false

        placeHolderLabel.topAnchor.constraint(equalTo: topAnchor
            , constant: textContainerInset.top).isActive = true
        placeHolderLabel.leadingAnchor.constraint(equalTo: leadingAnchor
            , constant: textContainerInset.left + padding).isActive = true
        placeHolderLabel.bottomAnchor.constraint(equalTo:  bottomAnchor
            , constant: textContainerInset.bottom).isActive = true
        placeHolderLabel.trailingAnchor.constraint(equalTo: trailingAnchor
            , constant: textContainerInset.right + padding).isActive = true
        
        let widthConstant = (textContainerInset.left + textContainerInset.right + padding * 2)
        placeHolderLabel.widthAnchor.constraint(equalTo: widthAnchor
            , constant: -widthConstant).isActive = true
        
        // Fontカラーはgrayにしていますが、必要に応じて変更してください
        placeHolderLabel.font = font
        placeHolderLabel.textColor = UIColor.gray
        placeHolderLabel.numberOfLines = 0
        placeHolderLabel.text = placeHolder

        // Observations
        // UITextViewの編集イベントを受け取るためにKVOの設定をしています
        // UITextViewの編集を始めるとプレースホルダーが消え、
        // 編集完了すると、textの中身をみてプレースホルダーを表示するか判断します
        NotificationCenter.default.addObserver(self, selector: #selector(hidePlaceHolder), name: UITextView.textDidBeginEditingNotification, object: nil)
        
        NotificationCenter.default.addObserver(self, selector: #selector(changePlaceholderVisibility), name: UITextView.textDidEndEditingNotification, object: nil)
    }
    
    @objc func hidePlaceHolder() {
        placeHolderLabel.isHidden = true
    }
    
    @objc func changePlaceholderVisibility() {
        placeHolderLabel.isHidden = !text.isEmpty
    }
}

以上で下記のようなプレースホルダー付きのUITextViewが簡単に使用できます。

f:id:Iganin:20190622220151p:plain
PlaceHolderTextView(プレースホルダー表示)
f:id:Iganin:20190622220219p:plain
PlaceHolderTextView(プレースホルダー非表示)

まとめ

プレースホルダー付きのUITextViewの作成方法をご紹介しました。

要件として上がってきた際にはぜひ使用してみてください。

参考

【iOS】単位付きのUITextFieldを作る方法とカスタムクラス

はじめに

ユーザーからの情報の入力の際にTextViewと並んでUITextFieldはよく使われます。 単位付きの入力にしたい場合は、UITextFieldの範囲外にUILabelを設置する、 入力値に単位をつけ、それを特別扱いするようDelegateメソッド内で 対応するといった方法がありますが、今ひとつ実装が面倒なのが正直なところです。

本記事では単位付きのUITextFieldを簡単に作成する方法とカスタムクラスのご紹介をします。

環境設定

以下の環境を使用しています。

  • Xcode10.2.1
  • Swift 5.0.1

方法

UITextFieldには、leftView, leftViewMode, rightView, rightViewModeというプロパティがあります。 それぞれUITextFieldの左側のView, 右側に設置するViewおよびそれらをどのように表示するかを指定できます。

なお、ViewModeには以下の設定値があります。

    public enum ViewMode : Int {
        case never // 出さない
        case whileEditing // 編集中のみ出す
        case unlessEditing // 編集中意外に出す
        case always // 常時出す
    }

したがって、leftView, rightViewにUILabelを設定し、適切なViewModeを設定することでUITextFieldに簡単に単位をつけることができます。 なお、leftView, rightViewはUIViewなので、それを継承したUIImageViewなども入れられるはずです。(試してはいません)

カスタムしたUITextField

アプリ作成時に簡単に使用できるように、単位を簡単につけられるUITextFieldを作成しました。

import Foundation
import UIKit

@IBDesignable
public final class WithUnitTextField: UITextField {
    
    @IBInspectable
    public var prefix: String?
    
    @IBInspectable
    public var suffix: String?
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    public override func awakeFromNib() {
        super.awakeFromNib()
        commonInit()
    }

    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
      
    public override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        commonInit()
    }
}

private extension WithUnitTextField {

    func commonInit() {
        // ViewMode常時出す設定にしているが必要に応じて変更すること
        // IBInspectableで設定できるようにしてもいいかもしれない
        
        let leftLabel = UILabel(frame: .zero)
        leftLabel.font = font
        leftLabel.text = prefix
        leftLabel.sizeToFit()
        leftView = leftLabel
        leftViewMode = .always
        
        // FontはUITextFieldのFontと同様にしているが、
        // こちらもIBInspectableで設定できるようにしてもいいかもしれない
        
        let rightLabel = UILabel(frame: .zero)
        rightLabel.font = font
        rightLabel.text = suffix
        rightLabel.sizeToFit()
        rightView = rightLabel
        rightViewMode = .always
    }
    
}

例えばですが、下記画像のようなTextFieldがInterface Builder上のみで簡単に作成できます。

f:id:Iganin:20190620003258p:plain

まとめ

単位付きのUITextFieldを作成する方法と、それを活かしたカスタムクラスのコードをご紹介しました。 要件として求められた際にはぜひ選択肢の一つとしてご検討ください。

参考

【iOS】SwiftGenで自動生成されるファイル内のStructやenumのアクセス修飾子をpublicにする方法

はじめに

SwiftGenはR.swift同様にAssetやImage、Storyboardへのアクセスを、安全にしてくれるStructやenumなどの自動生成ツールです。 例えば、UIImageのインスタンスの生成はSwiftGenを使用すると下記のようになり、タイポによる不具合を防ぐことができます。

// 前
let image = UIImage(named: "sample")

// 後
let image = Asset.sample.image

SwiftGenの設定方法等は他にも記事が上がっていますので、そちらをご参照ください。 本記事ではSwiftGenによって自動生成されるstructやenumをpublicアクセスにする方法を記載します。

環境設定

以下の環境を使用しています。

  • Xcode10.2.1
  • Swift 5.0.1

なぜpublicにしたいか

SwiftGenによって自動生成されるstructやenumのデフォルトのアクセス修飾子はinternalになっています。

この場合、マルチモジュール構成で画像やColor AssetをUIモジュールに分割していたり、Resourceと判断してそれ用のモジュールに分割していた場合、 そのモジュール外からでは自動生成されたstructやenumにアクセスできません。そこでアクセス修飾子をpublicにしたいというモチベーションが生まれます。

方法

SwiftGenを使用する際には各種設定をswiftgen.ymlファイルに記載しますが、こちらに修正を加えることでpublic accessに変更できます。

xcassets:
  inputs: UIComponent/Assets.xcassets
  outputs:
    templateName: swift4
    output: UIComponent/Generated/DefaultAsset.swift
    params: # ここで設定を行っています
      publicAccess: true

上記の設定内の params -> publicAccess: true で設定を行っています。これでアクセス修飾子を変更できます。

まとめ

SwiftGenを使用した際のアクセス修飾子の変更方法を記載しました。 publicAccess: trueにすれば良いことはわかりましたが、 paramsで設定するという情報になかなかたどり付けなかったため、まとめることにしました。

publicAccess以外にもparamsを使用することでtemplateの設定を上書きし、自動生成されるコードの内容を変えられるようです。ぜひ試してみてください。

参考

【iOS】RawReperesentableを使用してID間の使用ミスを避ける方法

はじめに

アプリを作成しているとクラスやStructの一意性の判別のためにidを良く使用します。 例えばUserを定義した場合、その一意性を決めるためにUser.idを定義します。 多くの場合はidはStringかIntで定義するかと思います。

idが一つだけならば良いのですが、 jobIdやgroupIdなど複数のIDを扱うようになると、 jobIdとgroupIdにString型を使用していた場合、それらのIdの入れ間違いが発生し得ます。 Idは一意性を担保するため、この間違いは大きな障害に繋がりかねません。

本記事ではこのような問題を発生させない方法を記載します。

環境設定

以下の環境を使用しています。

  • Xcode10.2.1
  • Swift 5.0.1

方法

import Foundation

typealias Identifiable = RawRepresentable & Codable & Equatable & Hashable

struct User: Codable {
    let id: ID
    let name: String
    let job: Job
    
    struct ID: Identifiable {
        typealias RawValue = String
        let rawValue: RawValue
    }
}

struct Job: Codable {
    let id: ID
    let name: String
    
    struct ID: Identifiable {
        typealias RawValue = String
        let rawValue: RawValue
    }
}

let userString = """
{
    "id": "11111",
    "name": "Tanaka",
    "job": {
        "id": "01",
        "name": "engineer"
    },
}
"""

let userData = userString.data(using: .utf8)!


let user: User
do {
    user = try JSONDecoder().decode(User.self, from: userData)
    print("User: \(user)")
    print("UserID: \(user.id.rawValue)")
    print("UserJobID: \(user.job.id.rawValue)")
} catch let error {
    fatalError(error.localizedDescription)
}

do {
    let encodedUserData = try JSONEncoder().encode(user)
    let encodedUserString = String(data: encodedUserData, encoding: .utf8)!
    print(encodedUserString)
} catch let error {
    fatalError(error.localizedDescription)
}

上記のようにすることで、 UserのidはUser.ID型、JobのidはJob.ID型となり別々の型となるためIDの間違いがなくなります。

IDをRawReperesentableに準拠させることで、文字列から直接User.ID型にDecodeしたり、User.IDから文字列としてのEncodeが可能になります。 また、ID型をEquatableとHashableに準拠させることで、ID自体の同値判定やDictionaryのkeyとしてID自体を使用することを可能にしています。

IDについては各Struct内に定義するのが良いかと思いますが、 IDとしてStringしか使用しないなどの取り決めがある場合は以下のようにすると定義回数が減らせるのでいいかもしれません。

struct ID<T>: Identifiable {
    let rawValue: String
}

struct User: Codable {
    id: ID<User>
}

またRawRepresentableに準拠する際には以下の書き方もできます。

struct ID: RawRepresentable {
    let rawValue: String
}

RawRepresentableについて

RawRepresentableに関して知識が曖昧だったため学習がてらまとめてみます。

  • rawValueとして指定された型へ、もしくは型から変換できる。
  • Enumの型としてString, Int, 不要小数点型(CGFloat, Floatなど)を指定した場合はCompilerが自動的にRawRepresentableを付与する。
  • OptionSet ProtocolはRawRepresentable Protocolに準拠している。

まとめ

IDの間違いは致命的なバグに繋がり得るため慎重な扱いが必要です。 IDを定義する際の選択肢として本記事の方法がお役に立てば幸いです。

参考

【iOS】表示されているテキストをコピーできるUIを作成する簡単な方法

はじめに

iOSアプリを作成していると文言の表示に通常UILabelを使用します。 ただ、そのままではラベル上の文言のコピーを行うことができず、利便性の面であまり満足できないような場合があります。 以下で文言をコピーできるUIを作成する方法を記載します。

環境設定

以下の環境を使用しています。

  • Xcode10.2
  • Swift 5.0

実装

いくつか方法があるかと思いますが、簡単な方法としてUILabelの代わりにUITextViewを使用する方法があります。 基本的にはUILabelの代わりにUITextViewを設定するだけですが、注意点がいくつかあります。

  • isEditable を falseにする
    • trueのままだとタップで編集できてしまうためそれを防ぎます
  • isScrollEnabled を falseにする
    • このパラメータをfalseにすることで、UILabelのように内部の文言によってUITextViewのサイズが決定するようになります

また、表示する文言の行数を制限する場合下記のように設定します。

textView.textContainer.maximumNumberOfLines = 2;
textView.textContainer.lineBreakMode = .byTruncatingTail

Storyboard上では下記の赤枠の設定を変更します。行数制限についてはcode上で設定を行います。

f:id:Iganin:20190313175528p:plain

実際の画面表示は下記のようになります。該当のText部分をドラッグするとCopy, 検索, Shareの選択肢が表示されます。 ただ、左右のマージンを見ると赤背景の部分と、緑背景の部分でテキスト部分の表示に差があります。 ともに左右10ptのConstraintを親Viewに対してかけていますが、TextViewとUILabelはViewとTextContentとのPaddingなどに差があるためこのような表示の違いが生まれます。

f:id:Iganin:20190407214813p:plain

この部分は、下記の設定によって変更することができます。

  • textContainerInset
  • textContainer.lineFragmentPadding
  • layoutManager.usesFontLeading
        // textView内のInset
        textView.textContainerInset = .zero
        
        // TextView内の左右のPadding
        // defaultの値は5.0
        // contentsに使用できるwidthの計算に使用される
        textView.textContainer.lineFragmentPadding = 0.0
        
        // 通常はleadingがfontに依存して変わる
        // usesFontLeadingをfalseにすることでfontに依存しないようになる
        textView.layoutManager.usesFontLeading = false

ただ、上記の設定もコード上から行う必要があります。そのため、IB上からは設定の状況が確認できません。 そこで下記のように IBDesignableのクラスを作成し、使用することでIB上から確認できるようになります。

import Foundation
import UIKit

@IBDesignable
class UILabelTextView: UITextView {
    
    @IBInspectable
    var numberOfLines: Int = 1 {
        didSet {
            textContainer.maximumNumberOfLines = numberOfLines
        }
    }
    
    // IBInspectableにはEnumであるNSLineBreakModeを使用できなかったためIntを経由しています。
    // より良い方法をご存知の方がいましたら共有いただけますと幸いです。
    @IBInspectable
    var lineBreakModeNumber: Int = 0 {
        didSet {
            textContainer.lineBreakMode = NSLineBreakMode(rawValue: lineBreakModeNumber) ?? .byWordWrapping
        }
    }
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        commonInit()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        commonInit()
    }
    
    private func commonInit() {
        textContainerInset = .zero
        textContainer.lineFragmentPadding = 0.0
        layoutManager.usesFontLeading = false
    }
}

なお、IBDesignableのクラスを作成する際は、IBを開いた際のビルドスコープを狭めるためにUIパーツをEmbeded Frameworkに分離するのがおすすめです。

まとめ

UILabelのような扱いができるUITextViewを使用して表示文言のコピーができるUIを作成する方法をまとめました。 開発の中で上記要件が出た際はぜひ使用してみてください。

参考

try! Swift Tokyo 2019 に参加しました

はじめに

3/21(木)- 3/23(土)に開催されたtry! Swift 2019に参加してきました。 try! SwiftはSwift関連の技術に関する技術カンファレンスです。

www.tryswift.co

今年はベルサール渋谷ファーストで開催されました。 3/21(木) - 3/22(金)はセッションメインのカンファレンス、 3/23(土)は午前中がWorkshopで、午後がpeerlabでした。 その感想になります。

Swiftの可能性の広さを知れた

iOSアプリ開発以外へのSwiftの適用事例がとても興味深かったです。 例えば、ハードウェアへの応用例としてRaspberry Piへの適用やServerSideSwift(SSS)などのセッションがありました。 Server Side Swiftは一時期よく耳にしましたが、最近の動向があまり終えておらず、状況が把握できていませんでしたが、 本カンファレンスでの講演を聞き、俄然やる気が出ました。 紹介されていた書籍も購入しました。5月末までくらいを目標に読み進めていきたいです。

Swiftの深い内容を知れた

アセンブリ周辺の話やStringの仕組み、importの種類に関することなどは普段アプリ開発をしている中ではなかなかじっくりと調べることがなく、 理解が曖昧な部分だなと感じていました。また、理解を深めるためのとっかかりがなかなか得られないでいましたが、 今回のセッションを通して知識を得るためのガイドのようなものを手に入れることができたと思っています。

あまり詳しくない分野を掘り進めるための手がかりが得られた

Swiftでの音の成形、Core DataやSiri ショートカットなど、なかなか触ることがなかったiOSMacOS周辺の技術に関して、 どのようにすれば良いかの手がかりを得られたのは今後iOSエンジニアとしてキャリアを積む中でとても役だつものだと感じています。

とにかくモチベーションが上がった

正直なところセッションの内容は理解が追いつかない部分も多くて、まだまだ学ばなければいけないことはたくさんあるなと思いました。 こんなに深く理解しているんだ、こんな考え方を持って働いているんだ、こんなすごい人たちがいるんだ、 そういった感情を持てる目標となる人たちにたくさん出会えたことはtry! Swiftに参加してよかったことの一つです。

交流を広げることができた

try! Swiftは国際カンファレンスのため、日本国外から来られている方もたくさんいました。 そのような方と交流を持つことができ、非常に刺激的な時間を過ごすことができました。 国境を超えても同じような感じだな、この点はかなり違うのだな、そういったことを知れたのは面白かったです。

今後学ぶこと

以下、try! Swiftを終えて、直近学んでいく予定のことです。 目標締め切りは5月末です。

最後に

著作や講演の発表、ブログ記事など日頃の業務で大変お世話になっている憧れのような方が同じ会場にたくさん集まっている非常に充実した時間でした。 今後も全力で楽しく学び作っていこうと、とてもモチベーションが上がっています。 try! Swift tokyo 2019に参加してよかったと心より思っています。登壇者のみなさま、運営のみなさま、関わってくださったみなさま、ありがとうございました!

SwiftにおけるArraySliceについて

はじめに

SwiftではArrayに対して、その配列の部分配列を取得しようとするとArrayではなくArraySliceが返却されます。 なぜArrayでないのか、ArraySliceを使用する理由はなんなのか気になったので調べてみました。

環境設定

以下の環境を使用しています。

  • Xcode10.0
  • Swift4.2

ArraySliceについて

Appleのドキュメントを確認してみます。

The ArraySlice type makes it fast and efficient for you to perform operations on sections of a larger array. Instead of copying over the elements of a slice to new storage, an ArraySlice instance presents a view onto the storage of a larger array. And because ArraySlice presents the same interface as Array, you can generally perform the same operations on a slice as you could on the original array.

For more information about using arrays, see Array and ContiguousArray, with which ArraySlice shares most properties and methods.

ArraySlice - Swift Standard Library | Apple Developer Documentation

重要な部分をかいつまんでみてみると、以下のようになります。

新しい領域を確保してArrayの要素をコピーするのではなく、ArraySliceはArrayへのviewを表します。
またArraySliceはArrayと同様なインターフェースを備えているためArrayに実施するのと同様な操作をArraySliceに行うことができます。

つまりArraySliceは新規でメモリ領域の確保は行わず、元の配列を参照しながら 始点のindexとCountを保持していると考えられます。

コードでの確認

上記の確認のため実際にポインタのアドレスを見てみました。 わかりやすさのために prefixを使用しています。 ポインタを確認すると、同一であることがわかります。 したがって、 a とその部分配列であるbは同一のアドレスをさしていることがわかります。

let a = [1, 2, 3, 4, 5]
var b = a.prefix(3)

print(UnsafePointer(a))
b.withUnsafeBufferPointer { (pointer) -> Void in
    print(pointer)
}
0x00006000011fcac0
UnsafeBufferPointer(start: 0x00006000011fcac0, count: 3)

値の代入を行った場合にどうなるかも確認してみました。 参照のみ保持している場合は元のArrayの値まで変わってしまうのでしょうか。 結果としては下記のように値を変更したタイミングで新規にメモリ領域が確保されました。

let a = [1, 2, 3, 4, 5]
var b = a.prefix(3)

print(UnsafePointer(a))
b.withUnsafeBufferPointer { (pointer) -> Void in
    print(pointer)
}
b[2] = 9
b.withUnsafeBufferPointer { (pointer) -> Void in
    print(pointer)
}
0x00006000010e0de0
UnsafeBufferPointer(start: 0x00006000010e0de0, count: 3)
UnsafeBufferPointer(start: 0x00006000026f2620, count: 3)

この動きはStructにおけるコピーオンライトと同様の動きになります。 つまり、変数への格納時ではなく値が変わった際にメモリ領域の確保が行われます。

let a = [1, 2, 3, 4, 5]
var c = a
print(UnsafePointer(a))
print(UnsafePointer(c))

c[0] = 1
print(UnsafePointer(c))
0x0000600000622500
0x0000600000622500
0x00006000006360a0

なぜArraySliceを使うのかの考え

一番の理由は単純にパフォーマンスではないかと思います。 あたらしいメモリ領域を確保し、そこにArrayの要素をコピーするよりは元のデータの参照を保持し、 そのどの部分配列を見れば良いかというデータのみを持つ方がパフォーマンスが良いのではないだろうかと直感的に考えます。

終わりに

ArraySliceで返ってくると Array型に変換しなければArrayで定義してあるメソッドの引数に渡すこと等ができず、 正直なところArrayで返してくれればいいのになと思っていたのですが、調べてみてArraySliceも必要だなと考えが変わりました。

IBOutletCollectionについて

はじめに

InterfaceBuilderの要素をコードと紐づけるために、IBOutlet、 IBActionが使われます。 ただ、複数の共通要素に対してはIBCollectionを使用すると便利です。 自分用のメモをかねて、IBCollectionに関してまとめます。

環境設定

以下の環境を使用しています。

IB要素の定義

IBOutlet - View要素とコードとの紐付け IBAction - ButtonのAction - Target紐付け IBOutletCollection - 複数View要素とコードとの紐付け

それぞれ、 UINibDeclarationsに以下のように定義されています。

#define IBOutlet
#define IBAction void
#define IBOutletCollection(ClassName)

// IBInspectable, IB_DESIGNABLEも定義されています
#define IBInspectable
#define IB_DESIGNABLE

IB Outlet Collection

IBOutlet, IBActionはStoryboard, Xibから紐づけることがおおいかと思います。  IBOutletCollectionも同様にStoryboard, Xibから紐づける際にIBOutletCollectionを選択して紐づけることが可能です。 またIBOutletCollectionはコード側に記載し、それをStoryboard,Xibとひもづけることもできます。

f:id:Iganin:20190130235856p:plain
IBOutletCollection例

@IBOutlet var buttons: [UIButton]!

IBOutlet, IBActionはweakで紐付けられることが多いですが、 IBOutletCollectionは弱参照では定義できないので注意が必要です。 ※配列による強参照での保持は問題ないのか、という疑問があります。このあたりご存知の方がいらっしゃいましたら共有ください。

格納順

IBOutletCollectionと紐づけた順で格納されていました。 ただ、この辺りはOS versionによって変わる可能性があるので、 tag等で並び替えできるようにしておいた方が安全かもしれません。

まとめ

似たような複数のView要素をIBOutletで接続すると、コードが煩雑になるため、 必要に応じてIBOutletCollectionを使用するとコードがスッキリします。