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

Swift勉強の始まり

とりあえず電卓のUIを作ってみたいと思う

1。Xcodeを起動する
image.png

2。Create a new Xcode projectを選ぶ
image.png

3。Single View Appを選ぶ
image.png

4。基本情報欄で適当に入力し、Nextを押す。
image.png

5。保存先を選ぶ前に、Source Controlのチェックを外す。Createを押す
image.png

6。ここの xxxx.storyboardをクリックする
image.png

7。真ん中の画面でスマホ画面の位置を適切に調整する
image.png

8。右上の+ボタンを押すと、Objectの一覧表がポップアップされる。
何度も調節したら。。
image.png

9。三角を押せばいい
image.png

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

【Swift】オプショナル型の使い方

オプショナル型とは?

オプショナル型はSwiftの特徴の一つであり、値がnilの可能性がある時に用いられる型です。
このオプショナル型はその値がnilである可能性を示してくれるため非常に便利な型なのですが、扱い方を間違えるとコンパイルエラーになったりバグの要因になります。

今回はそんなオプショナル型の使い方について考えていきます。

Optional < Wrapped >型を用いる時

まず下記のコードを見てください。このコードはRPGに出てくる武器の構造体です。定数として名前(name)、攻撃力(offensivePower)、追加効果(additionalEffect)が定義されています。
ここで注目すべき点はadditionalEffectのかたがString?となっている点です。この?がオプショナル型を表していて、この定数がnilの可能性があることを示しています。
このように値が定かではない値にはオプショナル型を定義していきましょう。

struct Weapon {
  let name: String
  let offensivePower: Int
  let additionalEffect: String?
}

暗黙的にアンラップされたOptional型

下記のコードを見ると変数の定義時に!を指定することでアンラップが暗黙的に行われています。
しかし初期化されていない値に対してアクセスしたため実行時エラーになっています。
このように暗黙的なアンラップは危険な面があります。しかし、初期化時に値が決まっていなくても、初期化以降に必ず値が設定される場合は用いたほうが、可読性も上がり効率的です。

class Sample {
  var value: String!
}

let sample = Sample()
sample.value //実行時エラー

以下が利用した方が良い例です。
このコードはStoryboard上に配置した要素から自動的に生成されるコードで、Storyboard上の要素と@IBOutlet属性とともに宣言されたプロパティと紐づけています。
このsomeLabelプロパティはインスタンス化される際には空ですが、その後にStoryboardから生成された値が設定されるので、暗黙的にアンラップして大丈夫です。
もしここでアンラップしなければオプショナルバインディングを別で行わなければならないため、そちらはコードを冗長にしないためにも止めておきましょう。

@IBOutlet weak var someLabel: UILabel!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITableViewをUIStackView + UIStackViewに置き換える

UITableViewにCustomCellゴテゴテで実装された画面を、UIScrollViewに置き換える機会がありましたので共有します。
主にUIStackViewへの追加で制約が入らなくてつまりました。

※UITableViewCellをそのまま使う前提で書いてます。
普通にViewを置く場合はもっと簡単にできると思います。

前説

まずUITableViewCellってコードから生成できるの…?

というところから始まりました。
途中、
制約全然効いてくれない!!
セルの高さが全部同じになっちゃう!!
横幅がおかしくなる!!!

とか色々やりながら、結果生まれたのが以下のコードです。

その結果生まれたのが以下のコードです。

コードととストーリーボードの例

ViewController.swift
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var stack: UIStackView!

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

        let row = 20
        for i in 0...row {
            let cell = Cell.init(style: .default, reuseIdentifier: "Cell", widht: self.stack.frame.width)
            cell.heightAnchor.constraint(equalToConstant: cell.frame.height).isActive = true
            cell.hogeLabel.text = "\(i)"
            print("add to stack:\(cell)")
            self.stack.addArrangedSubview(cell)
        }
    }
}
Cell.swift
import UIKit

class Cell:UITableViewCell {
    // セルにはラベル1つだけおいてある
    @IBOutlet weak var hogeLabel: UILabel!

    // ストーリーボードで配置した時の初期化処理※ストーリーボードからの初期化は想定してません
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.nibInit(width: 0.0)
    }

    // 横幅を受け取って初期化する(制約がうまく効かなかったので)
    init(style: UITableViewCell.CellStyle, reuseIdentifier: String?, widht:CGFloat) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.nibInit(width: widht)
    }

    // viewの初期化
    fileprivate func nibInit(width: CGFloat) {
        guard let view = UINib(nibName: "Cell", bundle: nil)
            .instantiate(withOwner: self, options: nil)
            .first as? UIView else { return }

        //セルの高さはセル側でランダムに決める※使うときはちゃんとConstとか作って高さ設定してね
        let height = Bool.random() ? 50:100

        view.frame = CGRect(x: 0, y: 0, width: width, height: CGFloat(height))
        self.frame = CGRect(x: 0, y: 0, width: width, height: CGFloat(height))
        self.addSubview(view)
    }
}

ストーリーボードは特に特別な設定はしておりません。
普通にScrollViewをおいて中にStackViewを配置するだけです。
スクリーンショット 2019-10-21 21.24.23.png


もっとこんな方法あるよ!!や、
ここちょっとやばいよ!!などございましたらお教えいただけますと幸いです。

ありがとうございました。

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

UITableViewをUIScrollView + UIStackViewに置き換える

UITableViewにCustomCellゴテゴテで実装された画面を、UIScrollViewに置き換える機会がありましたので共有します。
主にUIStackViewへの追加で制約が入らなくてつまりました。

※UITableViewCellをそのまま使う前提で書いてます。
普通にViewを置く場合はもっと簡単にできると思います。

前説

まずUITableViewCellってコードから生成できるの…?

というところから始まりました。
途中、
制約全然効いてくれない!!
セルの高さが全部同じになっちゃう!!
横幅がおかしくなる!!!

とか色々やりながら、結果生まれたのが以下のコードです。

その結果生まれたのが以下のコードです。

コードとストーリーボードの例

ViewController.swift
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var stack: UIStackView!

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

        let row = 20
        for i in 0...row {
            let cell = Cell.init(style: .default, reuseIdentifier: "Cell", widht: self.stack.frame.width)
            cell.heightAnchor.constraint(equalToConstant: cell.frame.height).isActive = true
            cell.hogeLabel.text = "\(i)"
            print("add to stack:\(cell)")
            self.stack.addArrangedSubview(cell)
        }
    }
}
Cell.swift
import UIKit

class Cell:UITableViewCell {
    // セルにはラベル1つだけおいてある
    @IBOutlet weak var hogeLabel: UILabel!

    // ストーリーボードで配置した時の初期化処理※ストーリーボードからの初期化は想定してません
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.nibInit(width: 0.0)
    }

    // 横幅を受け取って初期化する(制約がうまく効かなかったので)
    init(style: UITableViewCell.CellStyle, reuseIdentifier: String?, widht:CGFloat) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.nibInit(width: widht)
    }

    // viewの初期化
    fileprivate func nibInit(width: CGFloat) {
        guard let view = UINib(nibName: "Cell", bundle: nil)
            .instantiate(withOwner: self, options: nil)
            .first as? UIView else { return }

        //セルの高さはセル側でランダムに決める※使うときはちゃんとConstとか作って高さ設定してね
        let height = Bool.random() ? 50:100

        view.frame = CGRect(x: 0, y: 0, width: width, height: CGFloat(height))
        self.frame = CGRect(x: 0, y: 0, width: width, height: CGFloat(height))
        self.addSubview(view)
    }
}

ストーリーボードは特に特別な設定はしておりません。
普通にScrollViewをおいて中にStackViewを配置するだけです。
スクリーンショット 2019-10-21 21.24.23.png


もっとこんな方法あるよ!!や、
ここちょっとやばいよ!!などございましたらお教えいただけますと幸いです。

ありがとうございました。

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

クラスの継承とプロトコルの準拠の使い分け

抽象概念の共有

各クラスや構造体に共通の抽象概念を共有する場合、クラスでは親のクラスを継承しその親の特徴を子にも共有できるようにし、プロトコルではその決まりごとを準拠して共通のルール(特徴)を他の構造体などと統一することができます。
親の特徴を受け継ぎ、子でも使えるようにするこの機能は一見同じように見えますが、実際はどのような違いがあり、どのように状況に合わせて使っていけば良いのでしょうか???
今回はこれについて考えていきます。

プロトコルの準拠

Swiftの機能であるプロトコルを準拠した場合の利点について解説していきます。
まず最初にクラスで実装した際の欠点について考えていきます。

以下のコードを見て、どのあたりが問題でしょうか?

問題点は2点あります。
一つ目は、インスタンス化してはいけない親クラスのFoodがインスタンス化可能であること。
二つ目は、店ではなく自然に生えているberryクラスにもshop変数が継承されているということです。

それでは上記の2点を解決していくためにプロトコルを用いていきます。

class Food {
  var shop: String?
  func taste()
  func eat()
}

class Sushi : Food {
  override func eat() {
    print("dip a soy source")
  }
}

class Berry : Food {
  override func eat() {
    print("take from tree")
  }
}

以下のコードがプロトコルで実装したコードです。
親をプロトコルにしたため、誤ってインスタンス化される恐れがなくなりました。そして変数shopがSushiだけに準拠でき、必要のない要素を各構造体に入れずにすみました。また、Foodプロトコルを拡張しtasteメソッドを実装することで構造体に共通のメソッドの記述しなくてすみました。

protocol Shop {
  var shop: String { get set}
}

protocol Food {
  func taste()
  func eat()
}

extension Food {
  func taste() {print("tasty!")}
}

struct Sushi : Food, Shop {
  var shop: String
  func eat() {print("dip a soysource")}
}

struct Berry : Food {
  func eat() {print("take from tree")}
}

クラスの継承

クラスの継承を用いる時は複数の型同士で同一のストアドプロパティを共有したいときに利用します。
以下のコードのように継承するクラス達が同じ持ち主(ストアドプロパティ)である場合、クラスのような参照型を利用すると、同じストアドプロパティを参照するのでこのような結果になります。

このように同じ値を共有したい場合はクラスの継承を使用するべきです。一応プロトコルでも実装可能ではありますが、複雑で冗長なコードになるので使わないのが得策です。

class Item {
  var owner: String? {
    didSet {
      guard let owner = owner else {return}
      print("\(owner) has a this item."
    }
  }
}

class Phone : Item {}

class Shoes : Item{}

let phone = phone
phone.owner = "Sanetugu Nekoyasiki"

//実行結果
Sanetugu Nekoyasiki has a this item.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】クラスの継承とプロトコルの準拠の使い分け

抽象概念の共有

各クラスや構造体に共通の抽象概念を共有する場合、クラスでは親のクラスを継承しその親の特徴を子にも共有できるようにし、プロトコルではその決まりごとを準拠して共通のルール(特徴)を他の構造体などと統一することができます。
親の特徴を受け継ぎ、子でも使えるようにするこの機能は一見同じように見えますが、実際はどのような違いがあり、どのように状況に合わせて使っていけば良いのでしょうか???
今回はこれについて考えていきます。

プロトコルの準拠

Swiftの機能であるプロトコルを準拠した場合の利点について解説していきます。
まず最初にクラスで実装した際の欠点について考えていきます。

以下のコードを見て、どのあたりが問題でしょうか?

問題点は2点あります。
一つ目は、インスタンス化してはいけない親クラスのFoodがインスタンス化可能であること。
二つ目は、店ではなく自然に生えているberryクラスにもshop変数が継承されているということです。

それでは上記の2点を解決していくためにプロトコルを用いていきます。

class Food {
  var shop: String?
  func taste()
  func eat()
}

class Sushi : Food {
  override func eat() {
    print("dip a soy source")
  }
}

class Berry : Food {
  override func eat() {
    print("take from tree")
  }
}

以下のコードがプロトコルで実装したコードです。
親をプロトコルにしたため、誤ってインスタンス化される恐れがなくなりました。そして変数shopがSushiだけに準拠でき、必要のない要素を各構造体に入れずにすみました。また、Foodプロトコルを拡張しtasteメソッドを実装することで構造体に共通のメソッドの記述しなくてすみました。

protocol Shop {
  var shop: String { get set}
}

protocol Food {
  func taste()
  func eat()
}

extension Food {
  func taste() {print("tasty!")}
}

struct Sushi : Food, Shop {
  var shop: String
  func eat() {print("dip a soysource")}
}

struct Berry : Food {
  func eat() {print("take from tree")}
}

クラスの継承

クラスの継承を用いる時は複数の型同士で同一のストアドプロパティを共有したいときに利用します。
以下のコードのように継承するクラス達が同じ持ち主(ストアドプロパティ)である場合、クラスのような参照型を利用すると、同じストアドプロパティを参照するのでこのような結果になります。

このように同じ値を共有したい場合はクラスの継承を使用するべきです。一応プロトコルでも実装可能ではありますが、複雑で冗長なコードになるので使わないのが得策です。

class Item {
  var owner: String? {
    didSet {
      guard let owner = owner else {return}
      print("\(owner) has a this item."
    }
  }
}

class Phone : Item {}

class Shoes : Item{}

let phone = phone
phone.owner = "Sanetugu Nekoyasiki"

//実行結果
Sanetugu Nekoyasiki has a this item.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] カメラロールから取得した動画のトリミング

カメラロールから取得した動画のトリミング方法

調べた記事を見ていると、
UIImagePickerControllerとUIVideoEditorControllerを用いてやる的なことが書いてあったのだけど、実はUIImagePickerControllerだけでできるとのこと、、、

(参考記事が少なすぎる?)

とりあえずどちらでもできるようにしたので、どっちも書きます。。。
どちらも
iOS13.1.2で動作確認ずみ

UIImagePickerControllerとUIVideoEditorControllerを用いてカメラロールから取得した動画トリミング

いらない記述があるかもしれない。
参考記事 https://github.com/albertbori/iOS-Sample-Video-Trimmer

import UIKit
import Photos
import MobileCoreServices

//省略

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        //動画のとき
        if #available(iOS 11.0, *) {
            if let asset = info[.phAsset] as? PHAsset {

                let options = PHVideoRequestOptions()
                options.isNetworkAccessAllowed = true

                let manager = PHImageManager.default()
                manager.requestAVAsset(forVideo: asset, options: options) {asset, audioMix, info in
                    guard let asset = asset else {
                        print("asset is nil")
                        return
                    }

                    if let assetUrl = asset as? AVURLAsset {
                        let tempFolderUrl = URL(fileURLWithPath: NSTemporaryDirectory()).standardizedFileURL
                        let asset = AVAsset(url: assetUrl.url)
                        let outputUrl = tempFolderUrl.appendingPathComponent(assetUrl.url.lastPathComponent)
                        guard let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
                            print("次のビデオをエクスポートできませんでした: \(assetUrl.url)")
                            return
                        }
                        exporter.outputURL = outputUrl
                        exporter.outputFileType = AVFileType.mp4
                        exporter.exportAsynchronously {
                            DispatchQueue.main.async {
                                picker.dismiss(animated: true, completion: nil)
                            }

                            self.showEditor(for: outputUrl)
                        }
                    }
                }
            }
        }
    }

    func mimeTypeForPath(path: String) -> String {
        let url = NSURL(fileURLWithPath: path)
        let pathExtension = url.pathExtension

        if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension! as NSString, nil)?.takeRetainedValue() {
            if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
                return mimetype as String

            }

        }
        return "application/octet-stream"

    }

    func showEditor(for outputUrl: URL) {
        guard UIVideoEditorController.canEditVideo(atPath: outputUrl.path) else {
            print("ビデオは編集できません: \(outputUrl.path)")
            return

        }
        DispatchQueue.main.async {
            let vc = UIVideoEditorController()
            vc.videoPath = outputUrl.path
            vc.videoMaximumDuration = 15
            vc.videoQuality = UIImagePickerController.QualityType.typeIFrame960x540
            vc.delegate = self
            self.present(vc, animated: true, completion: nil)
        }
    }

    static func deleteAsset(at path: String) {
        do {
            try FileManager.default.removeItem(at: URL(fileURLWithPath: path))
            print("Deleted asset file at: \(path)")
        } catch {
            print("Failed to delete assete file at: \(path).")
            print("\(error)")
        }
    }

    //ビデオのURLからサムネイル画像を作成
    func previewImageFromVideo(_ url:URL) -> UIImage? {
        print("動画からサムネイルを生成(URL)")
        let asset = AVAsset(url: url)
        let imageGenerator = AVAssetImageGenerator(asset: asset)
        imageGenerator.appliesPreferredTrackTransform = true
        var time = asset.duration
        time.value = min(time.value, 2)
        do {
            let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil)
            return UIImage(cgImage: imageRef)
        } catch {
            print(error) //エラーを黙って捨ててしまってはいけない
            return nil
        }
    }


    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }

}


extension GameEditVC: UIVideoEditorControllerDelegate {

    //動画トリミング成功時
    func videoEditorController(_ editor: UIVideoEditorController, didSaveEditedVideoToPath editedVideoPath: String) {

        //エラーで2回これが行われるので、こういった処理をする
        if editorBool == false {
            editorBool = true
            return
        } else {
            editorBool = false
            print("このパスに保存完了: \(editedVideoPath)")
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                GameEditVC.deleteAsset(at: editor.videoPath)

            }
            let videoURL = URL(fileURLWithPath: editedVideoPath)
            print(videoURL) //<-危険な強制アンラップは可能な限り避ける
            guard let image =  previewImageFromVideo(videoURL) else {
                print("previewImageFromVideo(\(videoURL)) is nil")
                return
            }
            self.profileImages.append(image)
            DispatchQueue.main.async {
                self.collectionView.reloadData()
            }
            editor.dismiss(animated:true, completion: nil)
        }
    }

    // 動画トリミング失敗時の処理
    private func videoEditorController(editor: UIVideoEditorController, didFailWithError error: NSError) {
        print("an error occurred: \(error.localizedDescription)")
        dismiss(animated:true)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            GameEditVC.deleteAsset(at: editor.videoPath)
        }
    }

    // 動画トリミングキャンセル時の処理
    func videoEditorControllerDidCancel(_ editor: UIVideoEditorController) {
        dismiss(animated:true)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            GameEditVC.deleteAsset(at: editor.videoPath)
        }
    }

}

UIImagePickerControllerのみでカメラロールから取得した動画トリミング

普通にこちらがおすすめです。

参考記事:
https://qiita.com/k_kuni/items/08e66d4d8074bff2c4a3
https://stackoverflow.com/questions/40354689/swift-how-to-record-video-in-mp4-format-with-uiimagepickercontroller/40354948#40354948

またアンドロイド用に.movではなく.mp4で保存したいので、そちらもやるか、、、
そしてプラスでfirestorageに保存するところまで!!

かなりいろいろ見たけど以下でできたよ、、、(まじで疲れたwww)

完成コードはこれ


import UIKit
import FirebaseAuth
import Firebase
import FirebaseStorage
import Photos
import MobileCoreServices
import AVFoundation

//撮影完了時-------------------------------

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {


        if let url = info[UIImagePickerController.InfoKey.mediaURL] as? NSURL {
            self.encodeVideo(videoURL: url as URL)
            picker.dismiss(animated: true, completion: nil)
        }

    }

    func encodeVideo(videoURL: URL){
        indicator.startFullIndicator(view: view)
        let avAsset = AVURLAsset(url: videoURL)
        let startDate = Date()
        let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetPassthrough)

        let docDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        let myDocPath = NSURL(fileURLWithPath: docDir).appendingPathComponent("temp.mp4")?.absoluteString

        let docDir2 = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as NSURL

        let filePath = docDir2.appendingPathComponent("rendered-Video.mp4")
        deleteFile(filePath!)

        if FileManager.default.fileExists(atPath: myDocPath!){
            do{
                try FileManager.default.removeItem(atPath: myDocPath!)
            }catch let error{
                print(error)
                indicator.stopFullIndicator(view: view)
            }
        }

        exportSession?.outputURL = filePath
        exportSession?.outputFileType = AVFileType.mp4
        exportSession?.shouldOptimizeForNetworkUse = true

        let start = CMTimeMakeWithSeconds(0.0, preferredTimescale: 0)
        let range = CMTimeRange(start: start, duration: avAsset.duration)
        exportSession?.timeRange = range

        exportSession!.exportAsynchronously{() -> Void in
            switch exportSession!.status{
            case .failed:
                print("\(exportSession!.error!)")
                self.indicator.stopFullIndicator(view: self.view)
            case .cancelled:
                print("Export cancelled")
                self.indicator.stopFullIndicator(view: self.view)
            case .completed:
                let endDate = Date()
                let time = endDate.timeIntervalSince(startDate)
                print(time)
                print("Successful")
                print(exportSession?.outputURL ?? "")
                let profileMovieRef = self.gameUserStorageRef.child("profileImage_\(self.profileImages.count).mp4")
                profileMovieRef.putFile(from: exportSession!.outputURL!, metadata: nil, completion: { (metadata, error) in
                        if error != nil {
                            print("エラー:\(error!)")
                            self.indicator.stopFullIndicator(view: self.view)
                        } else {
                            profileMovieRef.downloadURL { (url, error) in
                                guard let downloadURL = url else {
                                    print("エラー:\(error!)")
                                    self.indicator.stopFullIndicator(view: self.view)
                                    return

                                }
                                self.indicator.stopFullIndicator(view: self.view)
                            }
                        }
                    }
                )
            default:
                break
            }

        }
    }

    func deleteFile(_ filePath:URL) {
        guard FileManager.default.fileExists(atPath: filePath.path) else{
            return
        }
        do {
            try FileManager.default.removeItem(atPath: filePath.path)
        }catch{
            fatalError("Unable to delete file: \(error) : \(#function).")
        }
    }

    //ビデオのURLからサムネイル画像を作成
    func previewImageFromVideo(_ url:URL) -> UIImage? {
        print("動画からサムネイルを生成(URL)")
        let asset = AVAsset(url: url)
        let imageGenerator = AVAssetImageGenerator(asset: asset)
        imageGenerator.appliesPreferredTrackTransform = true
        var time = asset.duration
        time.value = min(time.value, 2)
        do {
            let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil)
            return UIImage(cgImage: imageRef)
        } catch {
            print(error) //エラーを黙って捨ててしまってはいけない
            return nil
        }
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        picker.dismiss(animated: true, completion: nil)
    }


    func showAddAlertController() {
        let alertController = UIAlertController(title: "選択してください", message: "画像・動画を追加します", preferredStyle: .actionSheet)

        let albumButton = UIAlertAction(title: "画像を選択", style: .default) { (action: UIAlertAction!) in
            self.indicator.startFullIndicator(view: self.view)
            let sourceType:UIImagePickerController.SourceType = .photoLibrary

            if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.photoLibrary){

                let cameraPicker = UIImagePickerController()
                cameraPicker.sourceType = sourceType
                cameraPicker.mediaTypes = ["public.image"]
                cameraPicker.delegate = self
                self.present(cameraPicker, animated: true, completion: nil)
                self.indicator.stopFullIndicator(view: self.view)
            }
        }

        let movieButton = UIAlertAction(title: "動画を選択", style: .default) { (action: UIAlertAction!) in
            let sourceType:UIImagePickerController.SourceType = .photoLibrary

            if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.photoLibrary){

                let controller = UIImagePickerController()
                controller.sourceType = sourceType
                controller.mediaTypes=[kUTTypeMovie as String] // 動画のみ
                controller.delegate = self
                controller.allowsEditing = true
                controller.videoMaximumDuration = 10 // 10秒で動画を切り取る
                controller.videoQuality = UIImagePickerController.QualityType.typeMedium

                self.present(controller, animated: true, completion: nil)
            }
        }

        let cancelButton = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)

        alertController.addAction(albumButton)
        alertController.addAction(movieButton)
        alertController.addAction(cancelButton)

        //alertControllerを表示させる
        self.present(alertController, animated: true, completion: nil)
    }

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

UIScrollVIewの上でも、キーボードが現れたときにいい感じにスクロールする

備忘録兼紹介です。
UIScrollView × 複数のUITextFieldで、キーボードが出たときにスクロールさせる処理を紹介します。

動き

コード

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var scroll: UIScrollView!    // UITextFieldとかを配置するView
    @IBOutlet weak var stack: UIStackView!      // 表示サンプル用

    // ScrollViewのBottomの制約
    @IBOutlet weak var scrollViewBottomConstraints: NSLayoutConstraint!

    private var nowEditingField:UITextField!    // 編集中のUITextField
    private var offsetAdded:CGFloat?            // スクロール戻す時用

    override func viewDidLoad() {
        super.viewDidLoad()

        // 表示サンプルとして、UILabelを20個配置
        let lines = 20
        for count in 1...lines {
            let field = UITextField()
            field.placeholder = "\(count)"
            field.borderStyle = .roundedRect
            field.textAlignment = .center

            field.delegate = self // ← UITextFieldのdelegateを拾いたいので、delegateを設定する

            self.stack.addArrangedSubview(field)
            field.widthAnchor.constraint(equalToConstant: 250.0).isActive = true
        }
    }

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

        // キーボードが表示されるときと消えるときに動作させるメソッドを登録
        // キーボードの高さがほしかった
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(UIResponder.keyboardWillShowNotification)
    }
}

extension ViewController: UITextFieldDelegate {

    // テキストフィールドにフォーカスがあたった時に呼ばれる
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        self.nowEditingField = textField
        return true
    }

    // テキストフィールドからフォーカスが外れる時に呼ばれる
    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        self.nowEditingField = nil
        let offsetAdded = self.offsetAdded
        self.offsetAdded = nil
        var offset:CGPoint = CGPoint(x: 0, y: 0)
        if self.scroll.contentOffset.y - (offsetAdded ?? 0) < 0 {
            // 上端
            offset = CGPoint(x: self.scroll.contentOffset.x, y: 0)
        }else if self.scroll.contentOffset.y > (self.scroll.contentSize.height - self.scroll.frame.size.height - self.scrollViewBottomConstraints.constant) {
            // 下端
            offset = CGPoint(x: self.scroll.contentOffset.x, y: self.scroll.contentOffset.y)
        }else {
            // その他
            offset = CGPoint(x: self.scroll.contentOffset.x, y: self.scroll.contentOffset.y - (offsetAdded ?? 0))
        }

        self.scrollViewBottomConstraints.constant = 0
        self.scroll.setContentOffset(offset, animated: true)

        return true
    }

    // キーボードが表示される直前に呼ばれる ※textFieldShouldBeginEditingより後に呼ばれる
    @objc func keyboardWillShow(_ notification: Notification) {
        guard let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
            return
        }
        guard let field = self.nowEditingField else { return }

        let keyboardHeight = keyboardFrame.cgRectValue.height
        let keyboardTopLine = self.view.frame.height - keyboardHeight

        let bottomLine = field.frame.origin.y + field.frame.height
        let displayBottom = bottomLine - self.scroll.contentOffset.y

        if displayBottom > keyboardTopLine {
            let sub = displayBottom - keyboardTopLine
            self.offsetAdded = sub
            let offset = CGPoint(x: self.scroll.contentOffset.x, y: self.scroll.contentOffset.y + sub)

            self.scroll.setContentOffset(offset, animated: true)
            self.scrollViewBottomConstraints.constant = keyboardHeight
        }
    }

    // キーボードのリターンでキーボードを閉じる
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        self.view.endEditing(true)
        return true
    }
}


ありがとうございました。
よかったら使ってください。(もっといい実装があれば教えて下さい。)

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

UILabelが省略されているか判定する

意外と見つからなくて困ったので、自分なりの実装方法を共有します。

コード

extension UILabel {

    var isOverflowing:Bool {
        return (realLineCount() > self.numberOfLines)
    }

    private func realLineCount()-> Int {
        guard let font = self.font else { return 0 }
        guard let text = self.text, text != "" else { return 0 }
        let sizeForWidthCheck = CGSize(width: Int.max, height: Int(ceil(font.pointSize)))

        let oneLineWidth = text.boundingRect(with: sizeForWidthCheck,
                                             options: .usesLineFragmentOrigin,
                                             attributes: [NSAttributedString.Key.font: font],
                                             context: nil).width
        let boundingWidth = text.boundingRect(with: self.bounds.size,
                                              options: .usesLineFragmentOrigin,
                                              attributes: [NSAttributedString.Key.font: font],
                                              context: nil).width
        return Int(ceil(oneLineWidth / boundingWidth))
    }

}

解説

UILabel.textは省略されている場合でも全文を返してくれるので、それを使用します。
realLineCountメソッドで、省略せずに全文を描画したときの行数を計算します。

let oneLineWidth = text.boundingRect(with: sizeForWidthCheck,
                                             options: .usesLineFragmentOrigin,
                                             attributes: [NSAttributedString.Key.font: font],
                                             context: nil).width

boundingRectメソッドはStringを描画した場合のRectを返してくれます。
これを使って、1行で全部描画したときの横幅を計算します。1

return Int(ceil(oneLineWidth / boundingWidth))

それをboundingWidth(Labelに描画したときの横幅)で割って、行数を計算します。2
小数点以下はいらないので、繰り上げます。

var isOverflowing:Bool {
    return (realLineCount() > self.numberOfLines)
}

計算した行数を自身の行数(ここは実際に表示される行数)と比較し、
大きければ省略されているのでtrueを返します。

純正で用意して欲しいな……

ありがとうございました。


  1. widthがInt.maxになっておりますが、
    こんなに使うことないと思うので適宜小さめの値に変えたほうが良いかもしれません。 

  2. 行数が増えると若干の誤差が出ると思います。(多分) 

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

UIButtonのTitleを中央揃えにする

UIButtonのTitleはデフォルトで中央揃えですが、なぜか左に寄ってしまうバグがあったのでコード上で中央揃えに設定しました。

環境

  • Xcode 11.0
  • Swift 5.1

ボタンのタイトル中央揃え

hogeButton.titleLabel?.textAlignment = NSTextAlignment.center
で設定。

ボタンのタイトル上下中央揃え

上下の位置も、下に寄ってしまっていたのでプロパティで設定。
hogeButton.titleLabel?.baselineAdjustment = .alignCenters

おまけ

その他のよく使うボタンタイトルに関するプロパティをまとめました。
(ボタンのタイトルはUILabelなので、UILabelのプロパティを使っているだけですが)

//LabelやButtonのサイズに合わせてフォントサイズを可変にする
hogeButton.titleLabel?.adjustsFontSizeToFitWidth = true
label.numberOfLines = 1

//lineBreakModeの設定
//表示できる部分のみを切り取る
hogeButton.titleLabel?.lineBreakMode = .byClipping
//複数行の場合、文字単位で折り返す
hogeButton.titleLabel?.lineBreakMode = .byCharWrapping
//複数行の場合、単語単位で折り返す
hogeButton.titleLabel?.lineBreakMode = .byWordWrapping
//文末を...で省略する
hogeButton.titleLabel?.lineBreakMode = .byTruncatingTail

参考

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

Swift の非同期処理の例(複数パターン)

はじめに

こんにちは! iOSエンジニアのやまたつ です☺️
Oshidoriというアプリを個人で開発し、リリースしています!!!Oshidoriというアプリを個人で開発し、リリースしています!!!

非同期処理を書く方法っていくつかあるんだなあと思ってまとめてみたくなったので書きました!
実務だったり、個人アプリで使った非同期処理のパターンを書きたいと思います。
複数通信の時の処理も書きます。

Promise Kit が有能すぎて感動したので Promise Kit 贔屓になってます。ご了承ください。

クロージャを使って非同期処理を書く

hoge.swift
                         // ここがクロージャ ////////////////////
func fetchUser(id: Int, comletion: @escaping (User?,Error?)->()) {
  // ここで通信
  uesrInfoRep.fetch(id) { user, error in
    // 通信終わったら入ってくる
    guard let _ = error else { 
      // エラーの時は nil と、 errorを返す
      comletion(nil, error)
      return 
    }
      // ちゃんと値が返ってきたら user と nil を返す
    completion(user, nil)
  }
}

// こんな風に使う
// completionの引数が使えるようになる
fetchUser() { user, error in 
  guard let _ = error else {
    // エラー処理
    return
  }
  self.user = user
  tableView.reload()
}

メリット

  • 慣れたらお手軽にかける

デメリット

  • 処理が複雑になってネストすると追うのが辛い

本当にあった怖い話

半年前くらいに書いたコードを見返したらこれが出てきました。
つっこみどころも満載だし、ネストが深すぎて修正する気すらなくすコード。
これがダメなコードってわかるくらいには成長していました。。。
一番怖いのが、getAllInfo を呼び出しているところで completion の処理を何も書いていないこと。。。

func getAllInfo(messageId: String, completion: @escaping () -> ()) {
        userInfoRep.getUserInfo { (userInfo) in
            self.userInfo = userInfo
            self.roomRep.getRoomInfo(roomId: userInfo.roomId, completion: { (room) in
                self.roomInfo = room
                if userInfo.partnerId == room.partnerId {
                    var imageUrl: URL?
                    imageUrl = URL(string: room.partnerImageUrl)
                    if let url = imageUrl {
                        let tmpImageView = UIImageView()
                        Nuke.loadImage(with: url, options: ImageLoadingOptions(
                            placeholder: UIImage(named: "Oshidori_null"),
                            transition: .fadeIn(duration: 0.33)
                            ), into: tmpImageView, progress: { (response, tmp , tmp1) in


// 呼び出し元はこうなっていた(恐怖)
messageRoomService.getAllInfo(messageId: messageId) {}


DispatchGroup を使う

コメントにどのような処理か書いています。

func fetchEntryDetail(entry: Entry, completion: @escaping (Offer?, User?, Error?) -> Void){
        var offer: Offer!
        var user: User!
        // 非同期のグループ作るよ!!!
        let dispatchGroup = DispatchGroup()
        // 並列で実行できるよ〜
        let dispatchQueue = DispatchQueue(label: "queue", attributes: .concurrent)

        // 1つ目の並列処理入ります!
        dispatchGroup.enter()
        dispatchQueue.async {
            entry.offerRef.getDocument { (ss, err) in
                guard err == nil else { completion(nil, nil, err); return }
                guard let ss = ss else { completion(nil, nil, nil); return }
                guard let data = ss.data() else { completion(nil, nil, nil); return }
                offer = Offer(id: ss.documentID, document: data, entryStatus: .entried )
                // 1つ目終わりました〜
                dispatchGroup.leave()
            }
        }
        // 2つ目の並列処理入ります!
        dispatchGroup.enter()
        dispatchQueue.async {
            entry.userRef.getDocument { (ss, err) in
                guard err == nil else { completion(nil, nil, err); return }
                guard let ss = ss else { completion(nil, nil, nil); return }
                guard let data = ss.data() else { completion(nil, nil, nil); return }
                user = User(id: ss.documentID, dict: data)
                // 2つ目終わりました〜
                dispatchGroup.leave()
            }
        }

        // 上の二つの処理が終わった時(両方の dispatchGroup.leave() が呼ばれた時)実行される
        dispatchGroup.notify(queue: .main) {
            completion(offer, user, nil)
        }
    }

メリット

  • 複数処理にはむいていそう

デメリット

  • 書き方を覚えるのが少し辛い
  • leave し忘れたりすると、意図しない処理になったりする。気付きにくそう。

以下の記事を読みながら実務でコードを書きました!
わかりやすいです!
Swiftで複数の非同期処理の完了時に処理を行う

Promise Kit

スターも 12,000 もあり、最新更新も1ヶ月前ととても活発なライブラリです!
使ってみた感じとても良かったのでオススメです!!!
こちらドキュメントです!

こんなにネストだらけのコードが、

login { creds, error in
    if let creds = creds {
        fetch(avatar: creds.user) { image, error in
            if let image = image {
                self.imageView = image
            }
        }
    }
}

綺麗にかける!!!!

// 最初に
firstly {
    // ログインして
    login()
// ログインが終わったら
}.then { creds in
    // ログインの結果取得した creds を使う
    fetch(avatar: creds.user)
}.done { image in
    // fetch が終わったら、fetchしたimageを使える
    self.imageView = image
}

Promise Kit スゲ〜〜〜

複数通信もお手の物

クロージャだと

// これが終わったら
operation1 { result1 in
    // 2をやる
    operation2 { result2 in
        // 2が終わったらこれ。ネスト。。。
        finish(result1, result2)
    }
}

Dispatchだと

やっぱり少し複雑

var result1: !
var result2: !
let group = DispatchGroup()
group.enter()
group.enter()
operation1 {
    result1 = $0
    group.leave()
}
operation2 {
    result2 = $0
    group.leave()
}
group.notify(queue: .main) {
    finish(result1, result2)
}

PromiseKit だと

firstly {
    when(fulfilled: operation1(), operation2())
}.done { result1, result2 in
    //…
}

PromiseKit 最高〜〜〜☺️

最後に

Promise Kit とても良いライブラリなのでぜひ使ってみてください!!!
OSS開発に興味が出てきました。
PR出したりして貢献してみたいですね!!!

番外編

RxSwift 公式ドキュメント (RxSwiftできれば解決なんですけどね笑)
RxSwift入門(2) 非同期処理してみる

RxSwift は業務で使っていますが、完璧に理解できているには遠く及ばず。。。
きちんと勉強して、「RxSwift 最高〜〜〜☺️」 となる日が早く訪れるようにします。

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

オプショナル使い方集[Swift]

クラスで絞る

//一番親のViewがSCNViewだったら
let scnView = self.view as! SCNView
//そいつの背景青にする
scnView.backgroundColor = UIColor.blue

あとで追記

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