20200221のSwiftに関する記事は5件です。

AutoLayoutの競合??

はじめに

AutoLayoutはきちんと設定したのに、デバッグエリアをみると以下のようなメッセージが書かれていることはありませんか??

2020-02-21 22:56:34.775265+0900 TableView[13144:2779148] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x2807d8460 UILabel:0x104a1ae70'\U8679\U8272'.height == 32   (active)>",
    "<NSLayoutConstraint:0x2807d28a0 UIStackView:0x107203e70.height == 500   (active)>",
    "<NSLayoutConstraint:0x2807e80f0 UILayoutGuide:0x281de0000'UIViewSafeAreaLayoutGuide'.bottom == UIStackView:0x107203e70.bottom   (active)>",
    "<NSLayoutConstraint:0x2807e82d0 UIStackView:0x107203e70.top == UILayoutGuide:0x281de0000'UIViewSafeAreaLayoutGuide'.top   (active)>",
    "<NSLayoutConstraint:0x2807d8780 V:|-(0)-[UIStackView:0x104a0f890]   (active, names: '|':UITableViewCellContentView:0x104a1c810 )>",
    "<NSLayoutConstraint:0x2807d8820 V:[UIStackView:0x104a0f890]-(0)-|   (active, names: '|':UITableViewCellContentView:0x104a1c810 )>",
    "<NSLayoutConstraint:0x2807dc7d0 'UISV-canvas-connection' UIStackView:0x104a1d4e0.top == TableView.RainbowColorView:0x107203d00.top   (active)>",
    "<NSLayoutConstraint:0x2807dc820 'UISV-canvas-connection' V:[TableView.RainbowColorView:0x107203d00]-(0)-|   (active, names: '|':UIStackView:0x104a1d4e0 )>",
    "<NSLayoutConstraint:0x2807d9310 'UISV-canvas-connection' UIStackView:0x104a0f890.top == UILabel:0x104a1ae70'\U8679\U8272'.top   (active)>",
    "<NSLayoutConstraint:0x2807dc910 'UISV-canvas-connection' V:[UIStackView:0x104a1d4e0]-(0)-|   (active, names: '|':UIStackView:0x104a0f890 )>",
    "<NSLayoutConstraint:0x2807d93b0 'UISV-spacing' V:[UILabel:0x104a1ae70'\U8679\U8272']-(0)-[UIButton:0x104a1cbb0'\U3053\U3053\U3092\U30bf\U30c3\U30d7\U3057\U3066\U304f\U3060\U3055\U3044']   (active)>",
    "<NSLayoutConstraint:0x2807dc960 'UISV-spacing' V:[UIButton:0x104a1cbb0'\U3053\U3053\U3092\U30bf\U30c3\U30d7\U3057\U3066\U304f\U3060\U3055\U3044']-(0)-[UIStackView:0x104a1d4e0]   (active)>",
    "<NSLayoutConstraint:0x2807d9770 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x104a1c810.height == 64   (active)>",
    "<NSLayoutConstraint:0x2807e8190 'UIViewSafeAreaLayoutGuide-bottom' V:[UILayoutGuide:0x281de0000'UIViewSafeAreaLayoutGuide']-(0)-|   (active, names: '|':TableView.RainbowColorView:0x107203d00 )>",
    "<NSLayoutConstraint:0x2807e8050 'UIViewSafeAreaLayoutGuide-top' V:|-(0)-[UILayoutGuide:0x281de0000'UIViewSafeAreaLayoutGuide']   (active, names: '|':TableView.RainbowColorView:0x107203d00 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x2807d28a0 UIStackView:0x107203e70.height == 500   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

もしかしたら、制約が競合している可能性があるかもしれません。今回はその場合の、

  • 原因
  • 対処法

の2点に絞って説明していきます!
※説明が間違っていたら申し訳ありません、、、

環境

  • macOS Catalina(10.15.2)
  • xcode(11.3)
  • Swift5.1

原因

ボタンタップ前 ボタンタップ後
image.png image.png

今回の場合、ボタンをタップすると虹色が表示され、その途端に上記のメッセージが表示されました。
※虹色はUIStackViewとUILabelで管理しており、UIStackViewのheight = 500で設定しています。デフォルトでUIStackViewをisHidden = trueで設定しています。そのUIStackViewをUITableViewCellが管理しているという構造です。
上記のメッセージを読むと、下の方に

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x283fe4910 UIStackView:0x10f901490.height == 500   (active)>

と書かれています。読んでみると、UIStackViewのheight = 500の制約が原因みたいです。もう少し詳しく説明すると、UIStackViewを非表示から表示にすることでUITableViewCellの高さが再計算され、その高さとUIStackViewのheight = 500を同時に満たそうとしていることが原因みたいです。

対処法

方法は至って単純です。UIStackViewのheight = 500の制約のPriorityを下げるだけで解決します。
image.png

これにより、制約の競合が発生した場合にUIStackViewのheight = 500の優先順位が下がります。基本的にはこの辺のこともxcodeが計算してくれますが、不具合の原因にもなりかねないのできちんと対応したいところですね笑

参考

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

XcodeのProject間でUIコピペする方法

既に作成したProjectよりエディターエリアにStoryboardを表示させ、コピーするUIのSceneをクリックしxcodeのEditからcopyを選択して別のProjectのstoryboardをクリックしてペーストする。

更にxxxxx.swiftファイルもナビゲータエリアに元ファイルからドラック&ペーストで貼り付けダイアログの表示でcopyを指定すれば移行できます。

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

UserDefaultsの中身全てを雑に出力する

Swift5.0。
デバッグしていて、「あーとりあえずUserDefaultsの中身ドバッと出したいな……」と思いました。
UserDefaultsから普通に取り出そうとすると、Key名が必要となりますが、mapみたいなのないかな〜と思ったら、下記がありました。

雑に出力
print(UserDefaults.standard.dictionaryRepresentation())

lldbデバッガのpoコマンドでやるのも手です。
出力結果は載せません。
とにかく汚いですが、全部出ます。辞書型。
自分のアプリドメイン外のものも出てる? 気がしますが、とにかく全部出ます。

アプリ内で接頭詞持ってて、キー名は必ずそれが付ける運用になっていたら、下記のようにしぼると捗りそうです。

キー名で絞る
print(UserDefaults.standard.dictionaryRepresentation().filter { $0.key.hasPrefix("hoge") })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】TableViewCellにImageViewを追加してURLで指定された画像を表示する

概要

 この記事は初心者の自分がRESTful なAPIとswiftでiPhone向けのクーポン配信サービスを開発した手順を順番に記事にしています。技術要素を1つずつ調べながら実装したため、とても遠回りな実装となっています。

前回の でクーポンの画像も配信するAPIを作りました。
アプリ側もAPI側から取得した画像を画面に表示できるように改造します。

参考

環境

Mac OS 10.15
Swift5
Xcode11.1

手順

  • TableViewCellにUIImageViewを追加する
  • 追加したUIImageViewでとりあえず画像を表示させる
  • jsonで取得したURLを使って画像を表示させる
  • 動作確認

TableViewCellにUIImageViewを追加する

 Xcodeを開いてMain.storyboardでTableVIewCellにUIImageViewを追加します。合わせてデザインを修正してこれまでのクーポンの文字情報の下に画像を表示するようにします。

TableViewCellの Row Height を200pxから400pxに変更して縦を長くします。そして長くした部分にUIImageViewを追加します。Autoresizingで四隅を固定します。そして追加したUIImageViewに tag を付けます(重要)。Labelで4番まで使っているので、5番を割り当てました。

custum-tableviewcell.png

 次にViewController.swiftを開いて、tableViewの高さを指定しているtableView.rowHeight = [数字] の部分を Main.storyboardで指定したTableViewCellの Row Height の値に変更します。

tableView.rowHeight = 400 // Row Heightで指定した長さに変更

追加したUIImageViewでとりあえず画像を表示させる

 ViewController.swift の tableView にUIImageViewへ画像を表示する処理を追加します。下記の通り追加しました。基本はLabelに値を設定する方法を同じです。

 上で作った画像保存用のフォルダにサンプル画像を格納し、画像ファイルのフルパスをとりあえずベタ書きで指定しました。

// クーポン画像を表示
        let couponImageView = cell.viewWithTag(5) as! UIImageView
        let imageFileUrl = "/Users/XXXXX/workspace/amiApp/images/coupon-image-001.png"
        couponImageView.image = UIImage(named: imageFileUrl)

 変更を保存してXcodeでアプリを実行すると、クーポン情報の下に画像が表示されました!
check-app-view-001.png

画像ファイルのパスをベタ書きしているので、全てのクーポンに同じ画像が表示されていると思います。また、少しレイアウトが崩れているので後で修正しようと思います。

レスポンスのjsonで取得したURLを使って画像を表示させる

 TableViewCell や UIImageView の書き方は問題無いことが分かったので、ローカルフォルダのパスではなくAPIのレスポンスで取得した画像ファイルのURLを使って画像を取得するように変更します。

URLの取得から表示までのコードはこちらのようになります。

        // URLで指定されたクーポン画像を表示
        let couponImageView = cell.viewWithTag(5) as! UIImageView
        if let urlString = coupon["image"] as? String {
            let url = URL(string: urlString)

            do{
                let imageData = try Data(contentsOf: url!)
                couponImageView.image = UIImage(data: imageData)
            } catch {
                print("Error : Cat't get image")
            }
        } else {
            couponImageView.image = UIImage(named: "no-coupon-image.png") //nilの場合は固定画像表示
        }
        return cell
    }

 ポイントが幾つかあります。

 1つ目はif let urlString = coupon["image"] as? String { 〜の部分です。レスポンスに画像ファイルのURLが指定されていない場合に NULL参照でアプリが落ちるのを防ぐため、as?を使ったキャストとオプショナルバインディングを組み合わせて、画像ファイルのURLが指定されていない場合は代わりの画像を表示するようにしています。

as?はキャストする際、指定した型ならそのオプショナル型(nilが含まれるかもしれない)を返し、指定した型以外なら nilを返します。

オプショナルバインディングは下記のような書式となり、[オプショナル型の変数]が nilで無い場合に限り任意の処理を実行される事が出来ます。elseと組み合わせる事で、nilかnilで無いかで処理を分ける事も出来ます。

If let [任意の定数] = [オプショナル型の変数] {
    オプショナル型の変数がnilじゃなかった場合に実施したい処理
} 

 ポイントの2つ目はlet imageData = try Data(contentsOf: url!)の部分です。URLを使って画像を取ってくる処理です。画像を取得出来た場合はcouponImageView.image = UIImage(data: imageData)でImageViewに画像を表示させています。

動作確認

 変更を保存してXcodeでアプリを実行すると、クーポン情報の下に画像が表示されました! URLを設定していない2枚目のクーポンからはアプリ側で持たせた仮の画像が表示されています。また、シミュレータを iPhone 11 Pro Max から iPhone 11 Pro にしたらレイアウトがピッタリはまりました。

check-app-view-002.png

これで一通りクーポンアプリの実装ができたので。次は作ったAPIをクラウドのサーバで公開させようと思います。

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

GoogleCloudVisionAPIのサンプルをSwift5で書き直す

Google Cloud VisionのサンプルコードのレポジトリにはiOS用のサンプルが存在するのですが、これがまた最終更新日が3年前と古く、そのまま使おうとしてもビルドできないため書き直す必要がでてきます。

Swift5で書き直した場合

SwiftyJSONによるパースがdo-catchに対応したのでそれ用に書き直すのが主でしょうか。
他はインターフェースがちょっと変わった程度です。
正直、Swift4でも動く気はする。

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import UIKit
import SwiftyJSON


class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    let imagePicker = UIImagePickerController()
    let session = URLSession.shared

    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var spinner: UIActivityIndicatorView!
    @IBOutlet weak var labelResults: UITextView!
    @IBOutlet weak var faceResults: UITextView!

    var googleAPIKey = "YOUR_API_KEY"
    var googleURL: URL {
        return URL(string: "https://vision.googleapis.com/v1/images:annotate?key=\(googleAPIKey)")!
    }

    @IBAction func loadImageButtonTapped(_ sender: UIButton) {
        imagePicker.allowsEditing = false
        imagePicker.sourceType = .photoLibrary

        present(imagePicker, animated: true, completion: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        imagePicker.delegate = self
        labelResults.isHidden = true
        faceResults.isHidden = true
        spinner.hidesWhenStopped = true
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}


/// Image processing

extension ViewController {

    func analyzeResults(_ dataToParse: Data) {

        // Update UI on the main thread
        DispatchQueue.main.async(execute: {

            self.spinner.stopAnimating()
            self.imageView.isHidden = true
            self.labelResults.isHidden = false
            self.faceResults.isHidden = false
            self.faceResults.text = ""

            // Check for errors
            do {
                // Use SwiftyJSON to parse results
                let json = try JSON(data: dataToParse)

                // Parse the response
                print(json)
                let responses: JSON = json["responses"][0]

                // Get face annotations
                let faceAnnotations: JSON = responses["faceAnnotations"]
                let emotions: Array<String> = ["joy", "sorrow", "surprise", "anger"]

                let numPeopleDetected:Int = faceAnnotations.count

                self.faceResults.text = "People detected: \(numPeopleDetected)\n\nEmotions detected:\n"

                var emotionTotals: [String: Double] = ["sorrow": 0, "joy": 0, "surprise": 0, "anger": 0]
                var emotionLikelihoods: [String: Double] = ["VERY_LIKELY": 0.9, "LIKELY": 0.75, "POSSIBLE": 0.5, "UNLIKELY":0.25, "VERY_UNLIKELY": 0.0]

                for index in 0..<numPeopleDetected {
                    let personData:JSON = faceAnnotations[index]

                    // Sum all the detected emotions
                    for emotion in emotions {
                        let lookup = emotion + "Likelihood"
                        let result:String = personData[lookup].stringValue
                        emotionTotals[emotion]! += emotionLikelihoods[result]!
                    }
                }
                // Get emotion likelihood as a % and display in UI
                for (emotion, total) in emotionTotals {
                    let likelihood:Double = total / Double(numPeopleDetected)
                    let percent: Int = Int(round(likelihood * 100))
                    self.faceResults.text! += "\(emotion): \(percent)%\n"
                }

                // Get label annotations
                let labelAnnotations: JSON = responses["labelAnnotations"]
                let numLabels: Int = labelAnnotations.count
                var labels: Array<String> = []
                if numLabels > 0 {
                    var labelResultsText:String = "Labels found: "
                    for index in 0..<numLabels {
                        let label = labelAnnotations[index]["description"].stringValue
                        labels.append(label)
                    }
                    for label in labels {
                        // if it's not the last item add a comma
                        if labels[labels.count - 1] != label {
                            labelResultsText += "\(label), "
                        } else {
                            labelResultsText += "\(label)"
                        }
                    }
                    self.labelResults.text = labelResultsText
                } else {
                    self.labelResults.text = "No labels found"
                }
            } catch {
                self.labelResults.text = "Error \(error)"
            }
        })

    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let pickedImage = info[.originalImage] as? UIImage {
            imageView.contentMode = .scaleAspectFit
            imageView.isHidden = true // You could optionally display the image here by setting imageView.image = pickedImage
            spinner.startAnimating()
            faceResults.isHidden = true
            labelResults.isHidden = true

            // Base64 encode the image and create the request
            let binaryImageData = base64EncodeImage(pickedImage)
            createRequest(with: binaryImageData)
        }

        dismiss(animated: true, completion: nil)
    }

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

    func resizeImage(_ imageSize: CGSize, image: UIImage) -> Data {
        UIGraphicsBeginImageContext(imageSize)
        image.draw(in: CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height))
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        let resizedImage = newImage!.pngData()
        UIGraphicsEndImageContext()
        return resizedImage!
    }
}


/// Networking

extension ViewController {
    func base64EncodeImage(_ image: UIImage) -> String {
        var imagedata = image.pngData()

        // Resize the image if it exceeds the 2MB API limit
        if (imagedata?.count > 2097152) {
            let oldSize: CGSize = image.size
            let newSize: CGSize = CGSize(width: 800, height: oldSize.height / oldSize.width * 800)
            imagedata = resizeImage(newSize, image: image)
        }

        return imagedata!.base64EncodedString(options: .endLineWithCarriageReturn)
    }

    func createRequest(with imageBase64: String) {
        // Create our request URL

        var request = URLRequest(url: googleURL)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue(Bundle.main.bundleIdentifier ?? "", forHTTPHeaderField: "X-Ios-Bundle-Identifier")

        // Build our API request
        let jsonRequest = [
            "requests": [
                "image": [
                    "content": imageBase64
                ],
                "features": [
                    [
                        "type": "LABEL_DETECTION",
                        "maxResults": 10
                    ],
                    [
                        "type": "FACE_DETECTION",
                        "maxResults": 10
                    ]
                ]
            ]
        ]

        // Serialize the JSON
        let jsonObject = JSON(jsonRequest)
        guard let data = try? jsonObject.rawData() else {
            return
        }

        request.httpBody = data

        // Run the request on a background thread
        DispatchQueue.global().async { self.runRequestOnBackgroundThread(request) }
    }

    func runRequestOnBackgroundThread(_ request: URLRequest) {
        // run the request

        let task: URLSessionDataTask = session.dataTask(with: request) { (data, response, error) in
            guard let data = data, error == nil else {
                print(error?.localizedDescription ?? "")
                return
            }

            self.analyzeResults(data)
        }

        task.resume()
    }
}


// FIXME: comparison operators with optionals were removed from the Swift Standard Libary.
// Consider refactoring the code to use the non-optional operators.
fileprivate func < <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
    switch (lhs, rhs) {
    case let (l?, r?):
        return l < r
    case (nil, _?):
        return true
    default:
        return false
    }
}

// FIXME: comparison operators with optionals were removed from the Swift Standard Libary.
// Consider refactoring the code to use the non-optional operators.
fileprivate func > <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
    switch (lhs, rhs) {
    case let (l?, r?):
        return l > r
    default:
        return rhs < lhs
    }
}

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