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

[Swift]プロパティについて

はじめに

前回の記事にプロパティのことを説明しました。
しかし、プロパティの分類の仕方がもう一つあるのでそれについて紹介します。

プロパティの分類

プロパティは、値を保持するストアドプロパティと、値を保持しないコンピューテッドプロパティがあります。今回はその2つについて説明していきます。

ストアドプロパティ

以下のtateやyokoのプロパティは、ストアドプロパティです。

struct Area{
    var tate=10
    var yoko=10
}
var A_1=Area()//インスタンス化
var A_2=Area()//インスタンス化
A_1.tate=3
A_2.tate=7
print(A_1.tate)//3
print(A_2.tate)//7

ここで、プロパティの値が変更されるとき、変更前と変更後に値を監視する機能があります。

struct Area{
    var tate=10{
        willSet{
            print("old numver=",self.tate)
        }
        didSet{
            print("new number=",self.tate)
        }
    }
    var yoko=10
}
var A_1=Area()
A_1.tate=3

//実行結果
//old numver=10
//new number=3

willSetの{}では、プロパティの変更前に実行されます。
didSetの{}では、プロパティの変更後に実行されます。
また、このような仕組みをプロパティオブザーバと言います。

コンピューテッドプロパティ

コンピューテッドプロパティは、プロパティを初期化せずに、なんらかの計算をして値を返すプロパティです。
そして、そのプロパティにアクセスするタイミングで、値を保存することができます。
ほとんど関数みたいなもんですね

struct Area{
    var yoko=10
    var tate:Int{
        get{
            return yoko
        }
        set{
            self.yoko=newValue+2
        }
    }
}
var A_1=Area()
A_1.tate=9
print(A_1.tate)//11

プロパティに代入するときはset{}が呼び出されて、A_1.tateみたいに値にアクセスする時(代入する時以外)はget{}が呼び出されます。
A_1.tateでは9を代入しているのでSet{}が呼び出されます。
その時、9にアクセスするにはnewValueを用います。newValue=9です。
それがyokoに代入されるのでyoko=11になります。
最後に,A_1.tateにアクセスしているのでget{return yoko}が実行されます。
yoko=11なので実行結果は11になります。

また、set{}は省略可能で、get{}は必須です。

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

【Swift】チャットアプリでよくみるキーボードの動きに追随するTextFieldとButtonの実装

チャットアプリでよくみるキーボードの動きに追随するTextFieldとButtoonを実装してみました。

開発環境

Xcode 12.3
Swift 5

デモ

こんなやつです。

イメージ.GIF

コード

InputAccesoryViewController.swift
import UIKit

class InputAccesoryViewController: UIViewController {

    let outputTableView = UITableView()

    //プロパティにアクセスできるようにグローバル定数として定義
    let inputTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Aa"
        textField.backgroundColor = .systemGray6
        textField.borderStyle = .roundedRect
        return textField
    }()

    override var canBecomeFirstResponder: Bool { return true }

    var inputContainerView: UIView!

    //inputAccesoryViewの中身を作って返り値に入れます。
    override var inputAccessoryView: UIView? {

        if inputContainerView == nil {

            //容器となるContainerViewを作成
            inputContainerView = InputContainerView()
            inputContainerView.backgroundColor = .systemYellow

            //ContainerViewに定義しておいたTextFieldを追加
            inputContainerView.addSubview(inputTextField)

            //ContainerViewの高さがキーボードを閉じた時と開いた時で自動でリサイズする
            inputContainerView.autoresizingMask = .flexibleHeight

            //送信ボタンの追加
            let sendButton = UIButton(type: .system)
            sendButton.setImage(UIImage(systemName: "paperplane"), for: .normal)

            //Button Actionを追加
            sendButton.addTarget(self, action: #selector(didTapSend), for: .touchUpInside)

            //ContainerViewにSendButtonを追加
            inputContainerView.addSubview(sendButton)

            //それぞれの部品に制約を設定します
            sendButton.anchor(
                right: inputContainerView.trailingAnchor,
                paddingRight: 8,
                width: 50,
                height: 50)

            inputTextField.anchor(
                top: inputContainerView.topAnchor,
                left: inputContainerView.leadingAnchor,
                bottom: inputContainerView.layoutMarginsGuide.bottomAnchor,
                right: sendButton.leadingAnchor,
                paddingTop: 8,
                paddingLeft: 8,
                paddingBottom: 8,
                paddingRight: 8)
        }
        //作成したContainerViewを返り値に入れる
        return inputContainerView
    }

    var messages = [String()]

    override func viewDidLoad() {
        super.viewDidLoad()

        inputTextField.delegate = self
        outputTableView.dataSource = self

        outputTableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")

        //これでTableViewのスクロールに合わせてキーボードが閉じれるようになる
        outputTableView.keyboardDismissMode = .interactive
        view = outputTableView
    }

    @objc func didTapSend() {

        if let message = inputTextField.text, inputTextField.text != "" {
            messages.append(message)
            inputTextField.text = ""
            outputTableView.reloadData()
        }
    }
}

extension InputAccesoryViewController: UITextFieldDelegate {

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        didTapSend()
        textField.resignFirstResponder()
        return true
    }
}

extension InputAccesoryViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return messages.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "cell")
        cell.textLabel?.text = messages[indexPath.row]
        return cell
    }
}

InputContainerView.swift

InputContainerView.swift
import UIKit

class InputContainerView: UIView {

    //これは、inputAccesoryViewが自動レイアウト制約から適切なサイズになるために必要です
    override var intrinsicContentSize: CGSize {
        return CGSize.zero
    }
}

UIView Extension

制約を容易にする為にメソッドを作成しました。

UIView+.swift
import UIKit

extension UIView {

    func anchor(top: NSLayoutYAxisAnchor? = nil,
                left: NSLayoutXAxisAnchor? = nil,
                bottom: NSLayoutYAxisAnchor? = nil,
                right: NSLayoutXAxisAnchor? = nil,
                paddingTop: CGFloat = 0,
                paddingLeft: CGFloat = 0,
                paddingBottom: CGFloat = 0,
                paddingRight: CGFloat = 0,
                width: CGFloat? = nil,
                height: CGFloat? = nil) {

        translatesAutoresizingMaskIntoConstraints = false

        if let top = top {
            topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
        }

        if let left = left {
            leadingAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
        }

        if let bottom = bottom {
            bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
        }

        if let right = right {
            trailingAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
        }

        if let width = width {
            widthAnchor.constraint(equalToConstant: width).isActive = true
        }

        if let height = height {
            heightAnchor.constraint(equalToConstant: height).isActive = true
        }
    }
}

苦戦したところ

SafeAreaの部分に作成したContainerViewが被って表示され、意図した結果がなかなか出なかった。

textFeild.bottomAnchor
inputContainerView.layoutMarginsGuide.bottomAnchorに指定するのがミソでした。

参考

こちらの記事を参考にし、解決することが出来ました。
iPhone X how to handle View Controller inputAccessoryView



何か間違いやより良い方法がありましたら優しく教えていただけると幸いです?‍♂️

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

【Swift】timeIntervalSinceが見つからない??

もしかして

Data().timeIntervalSince

DateData と書き間違えている??

Date().timeIntervalSince

Date(). と書けばちゃんとサジェストされる(日付に関することなので)。

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

【RxSwift】Reactive の Extension を作成する際にメモリリークが発生した

環境

  • Xcode 12.3
  • RxSwift 5.1.1

目的

  • TabTestViewControllerself.viewDidLayoutSubviews() 呼び出しを検知しその際の self.index を流す Observable<Int> を作成したい
TabTestViewController.swift
// こんな感じで利用する currentIndexDidChange を Extension で実装
self.rx.currentIndexDidChange
    .subscribe(self.indexSubject)
    .disposed(by: self.disposeBag)

当初実装

  • 以下のように実装するとメモリリークが発生する
TabTestViewController+rx.swift
extension Reactive where Base: TabTestViewController {

    var currentIndexDidChange: Observable<Int> {

        // メモリリークが発生
        return sentMessage(#selector(base.viewDidLayoutSubviews))
            .map { _ in base.currentIndex }
            .distinctUntilChanged()
            .share(replay: 1)
    }
}

原因

  • base がクロージャ内部で強参照されていることによる

間違い: self を弱参照しようとした

  • 「クロージャの中では selfを弱参照」と手癖で実装しようとしたところコンパイルエラー
    • Reactive<Base> は構造体のため weak が使えない
TabTestViewController+rx.swift
extension Reactive where Base: TabTestViewController {

    var currentIndexDidChange: Observable<Int> {

        // 'weak' may only be applied to class and class-bound protocol types, not 'Reactive<Base>'
        return sentMessage(#selector(base.viewDidLayoutSubviews))
            .map { [weak self] _ in

                guard let self = self else { fatalError() }

                return self.base.currentIndex
            }
            .distinctUntilChanged()
            .share(replay: 1)
    }
}

解決: base を弱参照とする

  • TabTestViewController はクラスのため weak で弱参照することができる
TabTestViewController+rx.swift
extension Reactive where Base: TabTestViewController {

    var currentIndexDidChange: Observable<Int> {

        // viewDidLayoutSubviewsのタイミングでcurrentIndexの変化を検知する
        return sentMessage(#selector(base.viewDidLayoutSubviews))
            .map { [weak base] _ in

                guard let base = base else { fatalError() }

                return base.currentIndex
            }
            .distinctUntilChanged()
            .share(replay: 1)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コードベースでカスタムTableViewCellを作りたい!

はじめに

「TableViewControllerを配置しただけだと、何か質素なデザインになってしまう。」「セルとセルの空間をもっと空けたい。」こんな時、あると思います。ここではUITableViewCellを継承したクラスを作成し、好みのカスタムセルを作成する方法を共有します。「こうした方がいい!」と感じた場合はコメントで教えて欲しいです。

Simulator Screen Shot - iPhone 11 Pro Max - 2021-01-22 at 13.25.29.png
こんな感じのTableViewができます。(上にNavigationBarがありますが。)

作り方

UITableViewCellを継承したクラスを作成する

class CustomTableViewCell: UITableViewCell {

}

初期化の処理をかく

セルの色や文字の場所などのセットアップを行う関数を行うための場所というイメージです。

class CustomTableViewCell: UITableViewCell {

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

元々のViewを透明にする処理

ここではカスタムビューを表現する方法として、UITableViewCellが元々持っているUIView(contentViewという)とは別のUIViewを用意し、そのUIViewに色をつけるなどしています。なのでcontentViewを透明にし、見えないようにします。

class CustomTableViewCell: UITableViewCell {

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func layoutSubviews() {
        contentView.backgroundColor = .clear
        backgroundColor = .clear
    }//追加した
}

contentViewそのものをいじりカスタムセルを作成する方法がありそうですが、技術不足により実装できませんでした。ここら辺のコメント特に募集しています。

UIViewを用意し、準備する

ここからはコードベースでレイアウトを組むときにやっていることとあまり変わりません。私の過去の記事でもコードベースでレイアウトを組むことについて書いているので、もし詰まったら参考にして下さい。

private let backView = UIView()
private func setupUIView() {
        backView.backgroundColor = UIColor(255,189,40)
        backView.layer.cornerRadius = 5
        addSubview(backView)
        backView.snp.makeConstraints {
            $0.width.equalToSuperview().multipliedBy(0.8)
            $0.height.equalTo(contentView.snp.height).multipliedBy(0.9)
            $0.center.equalToSuperview()
        }
}

留意点及びやっていることとしては

  • レイアウトが楽になるライブラリSnapKitを使っている
  • セルの高さがcontentViewの0.9倍になるようにしている
  • セルの幅が画面サイズの0.8倍になるようにしている
  • 角丸をつけている

です。

テキストが表示できるようにUILabelを用意する

セルを表示するだけであれば上述したことだけでOKなのですが、表示させる物が一つは必要なのでUILabelを追加します。

let titleLabel = UILabel()
private func setupcellLabel() {
        titleLabel.font = UIFont.systemFont(ofSize: 20)
        backView.addSubview(titleLabel)
        titleLabel.snp.makeConstraints {
            $0.centerX.equalToSuperview().offset(30)
            $0.centerY.equalToSuperview()
            $0.width.equalToSuperview()
        }
}

セルから少し右に離れたところにテキストが表示されるようにしています。

セットアップ関数をイニシャライザにいれる

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupUIView()
        setupcellLabel()
}

ここまでで一応それっぽく動かすための準備はできました。一旦ここまでのソースコードを載せます。

import UIKit

class CustomTableViewCell: UITableViewCell {
    private let backView = UIView()
    let titleLabel = UILabel()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupUIView()
        setupcellLabel()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupUIView() {
        backView.backgroundColor = UIColor(255,189,40)
        backView.layer.cornerRadius = 5
        addSubview(backView)
        backView.snp.makeConstraints {
            $0.width.equalToSuperview().multipliedBy(0.8)
            $0.height.equalTo(contentView.snp.height).multipliedBy(0.9)
            $0.center.equalToSuperview()
        }
    }
    private func setupcellLabel() {
        titleLabel.font = UIFont.systemFont(ofSize: 20)
        backView.addSubview(titleLabel)
        titleLabel.snp.makeConstraints {
            $0.centerX.equalToSuperview().offset(30)
            $0.centerY.equalToSuperview()
            $0.width.equalToSuperview()
        }
    }
    override func layoutSubviews() {
        contentView.backgroundColor = .clear
        backgroundColor = .clear
    }
}

contentViewのハイライトをなくす

上の通り動かすと、contentViewがハイライト処理を行ってしまうため、セルを選択したときにしたの画像のようになってしまいます。
スクリーンショット 2021-01-22 14.09.34.jpg

これは良くない?
まずはハイライト消します。

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupUIView()
        setupcellLabel()
        self.selectionStyle = .none//追加した
}

次にセルが選択された際に先ほど自分で定義したUIViewがハイライト処理されるようにします。

override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: true)
        backView.backgroundColor = selected ? UIColor(255,226,146) : UIColor(255,189,40)
    }

backViewの背景色をセルが選択された時は少し明るめの色、そうでない時は元々の色となるようにします。
三項間演算子を使っていますが、全然if文でも書けます。

完成! あとはTabeViewControllerでこのクラスを使うだけです。

TableViewでの処理

UITableViewDataSourceの関数で以下のように書きます。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomTableViewCell//いつもと違う
        return cell
}

あとはregisterのところで

register(CustomTableViewCell.self, forCellReuseIdentifier: "cell")

のように書けば先ほど作ったカスタムセルを使うことができます。

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

【SwiftUI】NavigationViewの遷移のネストでbackボタンが複数生成されていくのにハマったのでメモ

はじめに

SwiftUIを触り始めてまだ1日目
Navigationでの画面遷移を行う際に愚直にコードを書いていたが、解決記事がさっと見つからなかったのでメモ

開発環境

  • Swift(version 5.3.1)
  • Xcode(version 12.2)

解決したいこと

Navigationのbackボタンが前画面のViewに更新されるのではなく、下に追加されてしまっている…
Swift01.gif
いやいや、困るよ(汗

解決する前

  • FirstView.swift
  • SecondView.swift
  • EndView.swift

以上の3画面で構成している

FirstView.swift
struct FirstView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: SecondView()) {
                Text("Go Second View")
            }
        }
        .navigationTitle("First View")
    }
}
SecondView.swift
struct SecondView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: EndView()) {
                Text("Go End View")
            }
        }
        .navigationBarTitle("Second View", displayMode: .inline)
    }
}
EndView.swift
struct EndView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

シンプルに実装したが何が原因なのか…

解決方法

なんかNavigationが2個生成されてる雰囲気だよね
FirstView.swiftNavigationView{}を記述しているからSecondView.swiftではもしかしていらない感じ??
NavigationLinkだけ残して動くのかな…

SecondView.swift
struct SecondView: View {
    var body: some View {
        //NavigationView {}削除!
        NavigationLink(destination: EndView()) {
            Text("Go End View")
        }
        .navigationBarTitle("Second View", displayMode: .inline)
    }
}

これで実行↓
Swift02.gif
うんうん、この挙動を求めていたの
無事に解決した〜!

最後に

多分、これであってるよね?
愚直に書いてるとこういうところにハマるね
まだまだ初心者だけど頑張ろう

Qiita初投稿…こんな感じの書き方でいいのかなw

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

【Model I/O】MDLMeshオブジェクトから頂点データを取得する方法

Model I/Oフレームワークの、MDLMeshオブジェクトから頂点の情報を取得するのが面倒だったので、メモ。

参考:
Read vertex positions with Model I/O
https://gist.github.com/algal/8e31ce035af0aafebc3736661ae9e3cd

mdlMeshにMDLMeshオブジェクトの値が入っているとして、これをXcodeのデバッガ(LLDB)上で取得したい場合。

頂点の数を取得する。

デバッガ
po mdlMesh.vertexCount
// 出力結果:54

バッファのデータのstrideを確認する。

デバッガ
po mdlMesh.vertexDescriptor.layouts.filter {($0 as! MDLVertexBufferLayout).stride != 0}
// 出力結果:
// ▿ 1 element
//   - 0 : <MDLVertexBufferLayout: 0x2824e4490 stride=32>

strideは32であることがわかった。

次のコードで、fromByteOffsetにオフセットを与えると頂点の情報が確認できる
n番目の要素を得たいときは、n * strideで取得できる。例えば2番目の要素を取得したいときは2*32を与える・

デバッガ
po mdlMesh.vertexBuffers.first!.map().bytes.load(fromByteOffset: 1*32, as: Float3.self)
// 出力結果:
// ▿ SIMD3<Float>(0.5, 0.5, 0.5)
//   ▿ _storage : SIMD4Storage
//     - _value : (Opaque Value)

2番目の頂点の座標は、x = 0.5, y = 0.5, z = 0.5であることがわかった。

最後に

NoteではiOS開発、とくにCoreML、ARKit、Metalなどについて定期的に発信しています。
https://note.com/tokyoyoshida

Twitterでも発信しています。
https://twitter.com/jugemjugemjugem

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

【SwiftUI】SwiftUI Appで`window.rootViewController`を用いた初期画面の表示

はじめに

アプリの初期画面をwindow.rootViewControllerを用いて表示することは採用したアーキテクチャにもよると思いますが、よくあることかと思います。
ただし、プロジェクト作成時にLife CycleSwiftUI Appを選択した場合に少し実装が必要だったため記しておきます。

実装

SwiftUI Appでプロジェクトを作成すると、以下のようなファイルが生成されます。
このままだとContentViewが初期表示画面になってしまいます。
例えばCoordinatorRooterパターン等を利用したい場合はAppDelegateSceneDelegateを経由したくなります。

SampleApp.swift
import SwiftUI

@main
struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

まず、AppDelegateSceneDelegateをそれぞれ自分で作成します。
自作したSceneDelegateが呼び出されるような実装をAppDelegateにしていきます。

SceneDelegate.swift
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = SampleViewController()
            window.makeKeyAndVisible()
            self.window = window
        }
    }

}
AppDelegate.swift
import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        let config = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
        config.delegateClass = SceneDelegate.self
        return config
    }

}

これであとは、AppDelegateが呼び出されるようにするだけです。
@UIApplicationDelegateAdaptorを利用します。

SampleApp.swift
import SwiftUI

@main
struct SampleApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
        }
    }
}

これで、SceneDelegateで指定しているSampleViewControllerが表示されるようになります。

おわりに

そもそもSwiftUI Appを選択しなければ良い話ですが、参考になる方がいればいいなと思います。
単純に起動時に処理がしたいだけであればScenePhaseというものもあるようです。
https://developer.apple.com/documentation/swiftui/scenephase

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

はじめてのSwiftLintで出てきたissuesたち

先日、しどろもどろでSwiftLintを導入してみたら、
ブワァーと出てきました、Warning!!
スクリーンショット 2021-01-20 20.35.52.png

44個もissues(=問題)があるよ!!
見た瞬間に心が折れたので、一旦閉じて翌日に持ち越してしまってごめんなさい。

44個すべてのイシューを確認して、全部修正するのめちゃくちゃ大変そうだなと思ったのですが、
よく見ると同じ文言が繰り返されていることに気づきました。
こう、ぱっと見、英語ばかりで「うえー!」ってなってしまったのは反省です。
よく見れば大したことないことが、わかってきました。

文言はたった5つでした。

Line Length Violation

行長違反

Line Length Violation: Line should be 120 characters or less: currently 127 characters

(翻訳)行は120文字以下である必要があります:現在127文字

Trailing Newline violation

末尾の改行違反

Files should have a single trailing newline.

(翻訳)ファイルには、末尾に1つの改行が必要です。

Unused Optional Binding Violation

未使用のオプションのバインディング違反

Prefer != nil over let _ = (unused_optional_binding)

(翻訳)let _ =(unused_optional_binding)よりも!= nil`を優先します

Vertical Whitespace violation

垂直空白違反

Limit vertical whitespace to a single empty line.

(翻訳)垂直方向の空白を1行の空行に制限します。

Comment Spacing Violation

コメント間隔違反

Prefer at least one space after slashes for comments.
(翻訳)コメントのスラッシュの後に少なくとも1つのスペースを優先します。

所感

いろいろ細かな制約があるんですね。
Human Interface Guidelinesとか見ながら理解を深めていきたいなと思いました。
今回をきっかけに気づいたのですが、
英語には抵抗ない方だと思っていたけど、意外と自覚なく抵抗感あったのだな、と。
iOSエンジニア、Swift触る人になるんだから、
この程度の英語で少しでも拒否反応を示したらだめだなぁっていうのが今回の学びです。

今後、また新しいissuesに遭遇したら追記していきます。

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

~個人メモ~ [swift] 関数・クロージャの基本的な仕様について

※この記事はswift学習中の筆者のノートです。

ちょっと振り返りたい時に、都度本(swift実践入門)を開いて該当ページを探すのがめんどくさかったので簡単に纏めました。

関数

関数とは

"名前を持ったひとまとまりの処理"のことで、外部から処理を呼び出し実行することができる機能のことをいう。

定義方法

関数はfuncキーワードで宣言、そして関数名、引数、戻り値の型、文を定義する

func 関数名(引数名1: , 引数名2: , ...) -> 戻り値の型 {
  関数呼び出し時に実行される文
  必要に応じてreturn文で戻り値を返却
}

例:数値を2倍する関数

func double(number x: Int) -> Int {
    return x * 2
}

double(number: 150)      // 300

引数について

仮引数と実引数

  • 関数の定義時に宣言するもの・・・仮引数
  • 関数の呼び出し時に指定するもの・・・実引数
func printInt(_ int: Int) {
    print(int)
}
// 関数の定義時に宣言している'int'が仮引数

printInt(1)
// 関数の呼び出し時に指定している'1'がが実引数

外部引数名と内部引数名

  • 外部からの呼び出し時に使用するもの・・・外部引数名
  • 関数内で使用されるもの・・・内部引数名
func sample(外部引数名 内部引数名: ) {
    関数の内部では内部引数名を使用する
}

sample(外部引数名: )

外部引数名は省略することもできる。
その場合は関数の定義時に(_ 内部引数名: )のように外部引数名に_を使用する。
呼び出し時には、:も省略し、値のみの記述で呼び出す。

可変長引数

引数定義の末尾に...を加えることでArrey型となる。
(実引数が複数あっても配列として受け取ることができる)

func printStrings(strings: String...) {    
    for string in strings {
        print("\(string)")
    }
}

printStrings(strings: "abc", "def", "ghi")
// 実行結果
// abc
// def
// ghi

クロージャ

クロージャ(closure)とは

"名前を持たないひとまとまりの処理"のこと。
特徴的なのは名前が不要ということ型推論により省略可能範囲が広いということ変数に代入でき、クロージャをパラメータとして受け取ることができるということなどが挙げられる。

定義方法

クロージャ式と呼ばれる、処理を{}で囲むフォーマットで引数・戻り値の型・文を定義する。

{ (引数名1: , 引数名2: , ...) -> 戻り値の型 in
    実行される文
    必要に応じてreturn
}

例:数値を2倍するクロージャ

let double = { (x: Int) -> Int in
    return x * 2
}

double(2)    // 4

型推論

決まった型にクロージャを代入する場合は、型を省略できる。

// 型を省略しない場合
let double: (Int) -> Int

double = { (x: Int) -> Int in
    return x * 2
}

double(2)   // 4

// 型推論により省略した場合

let triple: (Int) -> Int

triple = { x in
    return x * 3
}

triple(2)   // 6

実行方法

定義した定数名+()内に引数をいれ実行する。
渡された引数に対して処理が実行される。

let lengthString = { (mozi: String) -> Int in
    return mozi.count
}

lengthString("文字数を知りたい")   // 8

ちなみに同じ処理を関数にするとこんな感じ

func lengthString2(mozi2: String) -> Int {
    return mozi2.count
}

lengthString2(mozi2: "mozisuuwosiritai")   // 16

クロージャの引数

関数の引数との違いは以下の通り

  • 関数 => 外部引数名と内部引数名がそれぞれ指定できる
  • クロージャ => 内部引数名のみ指定できる

すなわちクロージャの場合は()内には値のみ入るということ

また、クロージャではデフォルト引数も指定できない仕様となっている。

簡略引数名

クロージャでは簡略引数名を利用し、引数名の定義も省略することが可能である。
簡略引数名は$にインデックス番号をつける。
※型推論できない場合はコンパイルエラーとなる。

let array = [1,2,3,4,5]
let newArray = array.map { $0 * 5 }
// 配列の要素の1番目を $0 と表現している。
// mapメソッドの引数がクロージャ
// mapメソッドは要素を一つ一つ取り出して実行するから、取り出された要素は毎回一つ目の要素となる$0
// map( {() -> Int In return $0 * 5 } ) みたいな形が省略前の形と思う
newArray // [5, 10, 15, 20, 25]

変数と定数のキャプチャ

クロージャ自身が定義されたスコープの変数の値はreturn後も保持される.

let counter: () -> Int
do {
    var count = 0
    counter = {
        count += 1
        return count
    }
}

counter()  // 1
counter()  // 2

引数としてのクロージャ

クロージャを関数や別のクロージャとして利用する場合の仕様として、
属性トレイリングクロージャがある

属性は2種類

クロージャの型の前に、@属性名を追記して指定する

@escaping
関数に引数として渡されたクロージャが、関数のスコープ外で保持される可能性があることを示す属性
この属性が付与されているかどうかで、コンパイル時にクロージャがキャプチャを行うか判断される

@autoclousure
引数をクロージャで包むことで遅延評価を実現するための属性

トレイリングクロージャ

引数のクロージャを外に出すこと。
最後の引数がクロージャの場合のみ可能。

func hoge(A: Int, B: Int, Cl: () -> Int { 
    クロージャの処理 
})

のような関数を呼び出したい場合。

以下、呼び出し時

//通常
hoge(A: 1, B: 5, Cl: { Print(Int) })

//トレイリングクロージャ
hoge(A: 1, B: 5) { Print(Int) }

と書く事ができる

引数が一つだけの場合、()も省略でき、クロージャを外にだせる。
hoge { clouser }みたいな形。
けっこうよく見かける形。可読性がよくなる。

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

英語が苦手な人のためのXcode便利機能 SpellCheck

はじめに

こんにちは!スーパー超絶英弱エンジニアのせーたろです!
profileをplofileと書いてしまう英語力の僕が、インターン先のつよつよエンジニアから教えてもらった便利機能を紹介します。

Spell Checkの使い方

Xcode11から導入されたSpell Checkという機能を使います。
スクリーンショット 2021-01-22 1.00.17.png

Edit < Format < Spelling and GrammarからCheck Spelling While Typingをオンにします。

これでスペルミスをしている箇所に赤い点線が表示されるようになります。
スクリーンショット 2021-01-22 1.03.54.png

修正

ダブルクリックで修正できます。
スクリーンショット 2021-01-22 1.04.21.png

ショートカットキー

command⌘ + ;でスペルミスの箇所がハイライト、
command⌘ + :でSpelling and Grammar のウィンドウが表示されます。
スクリーンショット 2021-01-22 1.06.15.png

さいごに

この便利機能のおかげで、レビューなどでスペルミスの指摘を減らすことができるのではないでしょうか!
よければ使って見てください!

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

Swift Packageのバージョンを最新に解決できない

概要

iOSアプリ開発のCI/CDを行うときは、xcode buildやfastlaneでビルドやテストを行うと思います。その際にプロジェクト(ワークスペース)が参照しているSwift Packageがなかなか狙い通りに解決されなかったため、備忘録として残します。

とりあえず結果

結論としてはxcodebuildやfastlaneのパッケージ解決の方法では難しいことがわかりました。Package.resolvedやDerivedDataを削除しても謎のファクターXを参照して、古いリビジョンを解決してしまいます。

最新リビジョンを取得するワークアラウンド

そのプロジェクトがSPMでない場合でも、Package.swiftを用意し、そこにSPMのパッケージを書きます。
で、アップデートして、Package.resolvedを移動させます。

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

import PackageDescription

let package = Package(
    name: "Hoge",
    platforms: [
        .iOS(.v14),
    ],
    dependencies: [
        .package(name: "Hoge", url: "https://xxxxxxxxx",
        .branch("master"))
    ]
)



swift package update
mv Package.resolved Hoge.xcworkspace/xcshareddata/swiftpm/
bundle exec fastlane tests

この場合、Package.resolvedを参照してほしいので、fastlaneの場合は、disableAutomaticPackageResolutionを有効にします。

Fastfile.
    run_tests(..,
              xcargs: "-disableAutomaticPackageResolution",
              ...
              )

これで最新版をなんとか解決できました!

パッケージ解決に関与してそうなパラメータ

Package.resolved

XXX.xcodeproj/xcshareddata/swiftpm/Package.resolved というパスにあるファイルです。基本的にこのファイルがある場合は、このファイルに記載されているリビジョンのものがチェックアウトされますが、無視する設定もできるようです。

DerivedData

いわゆるビルド時の中間ファイルです。これがあると最新を取得してきてくれないことがあります。 このパスは指定することができるようで、fastlaneだとcloned_source_packages_pathというパラメータで指定することができます。xcodebuildだとclonedSourcePackagesDirPathというパラメータです。

resolvePackageDependencies

xcodebuildコマンドに渡すパラメータです。このパラメータの挙動を正確に把握していないのですが、プロジェクトAがSwiftPackageBとSwiftPackageCをxcodeprojで参照しており、SwiftPackageBがSwiftPackageCを参照していた場合、プロジェクトAが参照するプロジェクトCのバージョンは、プロジェクトBのPackage.Swiftのバージョンになる、という挙動をしました。

というところまでは調べたのですが、ファクターXがなんなのかが結局わかりませんでした。

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