Swift

...now browsing by category

 

[Swift] Swift Package Manager

水曜日, 8月 1st, 2018

Swift Package Manager とは、Apple純正のSwift用ライブラリ管理ツールです。
CocoaPodsやCarthageみたいなものですね。
Swift3から使用できるそうなので、試して見ました。

ライブラリの作り方

まずは取り込まれるライブラリ側の作り方です。
コマンドラインにて、フォルダを作り、ライブラリ用に初期化します。
$ mkdir Increment
$ cd Increment
$ swift package init
Creating library package: Increment
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/Increment/Increment.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/IncrementTests/
Creating Tests/IncrementTests/IncrementTests.swift
Creating Tests/IncrementTests/XCTestManifests.swift
$

このままコマンドラインで開発を続けることもできるのですが、Xcode Projectを作って、Xcode上で開発できるようにしてみましょう。

$ swift package generate-xcodeproj
generated: ./Increment.xcodeproj
$

以下のようにするとコマンドライからXcodeで開けます。
$ open Increment.xcodeproj
$

デフォルトで Sources/Increment/Increment.swift が作成されているので、書き換えます。
今回はInt型にincrementメソッドを追加してみました。

extension Int {
    public var increment: Int {
        return self + 1
    }
}

テストコード(Tests/IncrementTests/IncrementTests.swift)も書きましょう。

import XCTest
@testable import Increment

final class IncrementTests: XCTestCase {
    func testExample() {
        XCTAssertEqual(1.increment, 2)
    }

    static var allTests = [
        ("testExample", testExample),
    ]
}

書き換えた後はテストを実行して、ビルドおよびテストが通ることを確認してください。
コマンドライの場合は、以下の通りです。
$ swift test
Compile Swift Module 'Increment' (1 sources)
Compile Swift Module 'IncrementTests' (2 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/IncrementPackageTests.xctest/Contents/MacOS/IncrementPackageTests
Test Suite 'All tests' started at 2018-08-01 08:08:00.046
Test Suite 'IncrementPackageTests.xctest' started at 2018-08-01 08:08:00.047
Test Suite 'IncrementTests' started at 2018-08-01 08:08:00.047
Test Case '-[IncrementTests.IncrementTests testExample]' started.
Test Case '-[IncrementTests.IncrementTests testExample]' passed (0.206 seconds).
Test Suite 'IncrementTests' passed at 2018-08-01 08:08:00.253.
Executed 1 test, with 0 failures (0 unexpected) in 0.206 (0.207) seconds
Test Suite 'IncrementPackageTests.xctest' passed at 2018-08-01 08:08:00.254.
Executed 1 test, with 0 failures (0 unexpected) in 0.206 (0.207) seconds
Test Suite 'All tests' passed at 2018-08-01 08:08:00.254.
Executed 1 test, with 0 failures (0 unexpected) in 0.206 (0.207) seconds

最後にgitリポジトリを作って、’1.0.0’として登録しておきます。

$ git init
Initialized empty Git repository in /Users/hide/Documents/tmp/SoftwareDesign/sd201807/HelloWorldLibrary/.git/
$ git add .
$ git commit -am 'Version 1.0.0'
[master (root-commit) 7a9fdf6] Version 1.0.0
7 files changed, 70 insertions(+)
create mode 100644 .gitignore
create mode 100644 Package.swift
create mode 100644 README.md
create mode 100644 Sources/HelloWorldLibrary/HelloWorldLibrary.swift
create mode 100644 Tests/HelloWorldLibraryTests/HelloWorldLibraryTests.swift
create mode 100644 Tests/HelloWorldLibraryTests/XCTestManifests.swift
create mode 100644 Tests/LinuxMain.swift
$ git tag 1.0.0

アプリの作り方

続いて、ライブラリを使うアプリ側の作り方です。
初期化のオプションに –type=executable が付くのがライブラリとは異なります。

$ mkdir IncrementApp
$ cd IncrementApp
$ swift package init --type=executable
Creating executable package: HelloWorldApp
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/HelloWorldApp/main.swift
Creating Tests/
$

ここで Package.swift ファイルが作られているので、それを書き換えます。
デフォルトでは以下のようになっています。

// swift-tools-version:4.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "HelloWorldApp",
    dependencies: [
        // Dependencies declare other packages that this package depends on. 
        // .package(url: /* package url */, from: "1.0.0"),
    ],  
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "HelloWorldApp",
            dependencies: []),
    ]   
)

package.dependencies[]にIncrementへのパスとバージョンを記述します。
ここではローカルのパスを指定していますが、GitHubのリポジトリを指定することも可能です。

    dependencies: [
        // Dependencies declare other packages that this package depends on. 
        .package(url: "../Increment", from: "1.0.0"),
    ],  

package.targets[0].target.dependenciesにて、IncrementAppで使用するライブラリを記述します。

        .target(
            name: "IncrementApp",
            dependencies: ["Increment"]),

続いて、Sources/IncrementApp/main.swiftにアプリの実行コードを記述します。
1から10までの数値をインクリメントしたものを表示します。

import Increment

_ = (1...10).map { print($0.increment) }

これで完成です。
最後にアプリを実行して見ます。
$ swift run
Compile Swift Module 'IncrementApp' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/IncrementApp
2
3
4
5
6
7
8
9
10
11

参考

  • Software Designe 2018年7月号

[Swift] Arrayのインデックスに範囲外の値を入れてもクラッシュしないようにする

日曜日, 10月 22nd, 2017

概要

配列の要素のアクセスにて、クラッシュする”[]”の代わりに、nilを返す”[safe: ]”を追加して使う方法です。

序文

SwiftではArrayのインデックスに範囲外の値を指定するとクラッシュします。

    let array = ["A", "B", "C"]
    let index = 3
    let item = array[index]  // EXC_BAD_INSTRUCTION でクラッシュ

そのため、要素にアクセスする前に、インデックスのチェックを行う方法があります。

    let array = ["A", "B", "C"]
    let index = 3
    guard array.indices.contains(index) else {
        // インデックスの範囲外なら、nilを返す
        return nil
    }
    let item = array[index]  // 実行されない
}

面倒なのでついついサボって省略してしまい、後でクラッシュすることがあります。

実装

そこで、安全にアクセスできる”[safe: ]”を追加して、”[]”の代わりに使います。
これはインデックスが範囲外の場合、nilを返してくれます。
“[]”(subscript(Self.Index))はCollectionプロトコルのメソッドなので、同様にCollectionプロトコルに追加します。

extension Collection {
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

用例

“[]”の代わりに”[safe: ]”を使うと、インデックスが範囲外でもクラッシュせずにnilを返します。

    let array = ["A", "B", "C"]
    let index = 3
    let item = array[safe: index]  // nilを返す

注意

ちなみに配列の要素にnilが入っていた場合、戻り値の型は”Type??”になります。

    let array = ["A", "B", "C", nil]
    let index = 3
    let item = array[safe: index]  // item : String??

itemに対する最初のunwrapでnilならインデックスが範囲外、2度目のunwrapでnilなら要素がnilです。

    guard let firstUnwrappedItem = item else {
        return nil  // インデックスが範囲外
    }
    
    guard let secondUnwrappedItem = firstUnwrappedItem else {
        return nil  // 要素がnil
    }

追記

「そもそも範囲外のインデックスにアクセスするのはバグなのだから、nilを返すよりクラッシュさせてしまうべき」との意見もあるのですが、ユーザーにとってはアプリがクラッシュするのは最悪の事態なので、クラッシュするよりも動かない(nilチェックでリターンさせているとほとんどこの動きになります)方がはるかに良いと考えてます。

これは組込み系でよくある、フェイルセーフの考え方が主軸になっています。

参考

xcode – Safe (bounds-checked) array lookup in Swift, through optional bindings? – Stack Overflow

XcodeでSwiftのリファクタリングができないから、renameの代わりにターミナルのコマンドで一括置換してみた

木曜日, 9月 17th, 2015

Xcode7の正式版が出たというのに、未だにSwiftのリファクタリング機能が実装されていません。特にrenameはよく使うので困りものです。さすがにちまちま置換していくのはとても面倒なので、ターミナルのコマンドで一括置換しました。

find -E . -regex ".*\.(swift|storyboard)" | xargs perl -i -pe 's/\bOldName\b/NewName/g'

これでカレントディレクトリ配下の *.swift ファイルおよび *.storyboard ファイル内の、「OldName」を「NewName」に置換できます。

「.*\.(swift|storyboard)」の部分で対象とするファイルを指定しています。「-E」「-regex」 は拡張正規表現を使うためのオプションで、両方必要なようです。

「OldName」の前後の「\b」は単語の区切りを示します。これがあると「HogeOldName」や「OldNameMoge」などは対象外になります。

最初、perlの代わりにsedを使おうと思っていたのですが、正規表現で「\b」が使えなかったり、バックアップファイルが残ってしまったりするので、perlに変えました。

NSRange を Range<String.Index> に変換

火曜日, 4月 7th, 2015

Swift でちょっと苦労したのでメモ。

UITextFieldDelegate で返される NSRange を String の操作に使おうとしたら、そのままでは使えませんでした。調べてみると String の操作メソッドでは Range<String.Index> が使わていました。文字の位置を表す型として int や NSInteger ではなく、String.Index という専用の型が設けられたようです。

int から String.Index には直接変換できないので、advance関数を利用します。苦労の末に以下の方法で NSRange から Range<String.Index> への変換が出来ました。

    func rangeWith(range: NSRange) -> Range<String.Index> {
        var string = "dummy"
        var startIndex = advance(string.startIndex, range.location)
        var endIndex = advance(startIndex, range.length)
        return Range(start: startIndex, end: endIndex)
    }

ダミーのString使ったりするのはイマイチなので、なんか良い方法はないかなー