【iOS】IBDesignableで画像をscaleAspectFitやscaleAspectFillで表示できるUIButtonを作る

はじめに

通信結果から画像を取得し、その画像をAspectFill(もしくはAspectFit)で設定して、その部分を押下すると詳細画面に遷移するというのはアプリ開発においてよくあるパターンだと思います。 例えば、メディア系のアプリでなんらかの特集をくみ、その特集の写真をタップすると詳細記事に遷移するといった場合です。

このような場合の一つの対応方法はUIViewの上にUIImageViewを貼り、その上に同じ大きさのUIButtonを載せるというものです。 ただ、このような実装は単純に手間なだけでなく、ボタン押下時の自然なアニメーション(全体に薄暗い反転がかかるもの)がなくなってしまいがちです。 本記事ではUIButtonの拡張クラスを用意し、UIButtonそのものに上記のような表示の機能を持たせる方法を記載します。

環境設定

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

  • Xcode10.2.1
  • Swift 5.0.1

方法

肝となる部分は、UIButtonの設定項目である、 contentHrozontalAlignmentcontentVierticalAlignmentです。 これらは、それぞれButton内部のコンテンツ(titleLabel、imageView)をUIButton内にどのように配置するかを決定します。 デフォルトではこれらの値はそれぞれ .center.centerになっており、画像のサイズ以上に拡大することはできません。 それぞれを.fill, .fillとすることでボタンのサイズに追随して画像サイズが決定するようになります。

枠いっぱいまで広がった際に、画像部分がどのように表示されるか(短い方に合わせてAspect比を保って画面いっぱい、 長い方に合わせてAspect比を保って画面いっぱい)はUIButtonimageView.contentModeを設定することで指定できます。

あとは通信取得した画像をUIButtonに設定すれば「はじめに」に記載した様なUIを実現できます。 ※余談ですがURLから取得した画像の設定は例えばKingfisherでは button.kf.setImage(with: url, for: .normal)のように実現できます。

import Foundation
import UIKit

@IBDesignable
public final class ImageDesignableButton: UIButton {
    
    // MARK: - IBInspectable
    
    /// 画像部分のCornerRadius
    @IBInspectable
    public var imageCornerRadius: CGFloat {
        set { imageView?.layer.cornerRadius = newValue }
        get { return imageView?.layer.cornerRadius ?? 0.0 }
    }
    
    /// 画像部分のBorderWidth
    @IBInspectable
    public var imageBorderWidth: CGFloat {
        set { imageView?.layer.borderWidth = newValue }
        get { return imageView?.layer.borderWidth ?? 0.0 }
    }
    
    /// 画像部分のBorderColor
    @IBInspectable
    public var imageBorderColor: UIColor {
        set { imageView?.layer.borderColor = newValue.cgColor }
        get { return UIColor(cgColor: imageView?.layer.borderColor ?? UIColor.clear.cgColor) }
    }
    
    /// Button部分と画像部分のpadding
    @IBInspectable
    public var padding: CGFloat = 0 {
        didSet {
            // imageEdgeInsetsを設定してボタンの外枠と画像部分に隙間を作っています
            imageEdgeInsets = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding)
        }
    }
    
    // MARK: - Life Cycle
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    public override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        commonInit()
    }
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    public override func awakeFromNib() {
        super.awakeFromNib()
        commonInit()
    }
}

// MARK: - Private Function
private extension ImageDesignableButton {
    
    func commonInit() {
        // 画像部分の設定をしています
        // この部分はIBInspectableで外にだしてもいいかもしれません
        // image
        imageView?.contentMode = .scaleAspectFill
        
        // horizontal, verticalそれぞれの設定値を.fillとする
        // ことでコンテンツ部分いっぱいに画像が配置されるようになります
        contentHorizontalAlignment = .fill
        contentVerticalAlignment = .fill
        
        // 画像の外枠を丸くしたり、枠を作ったりする設定です
        // 必要に応じて使ってください
        // image layer
        imageView?.layer.borderColor = imageBorderColor.cgColor
        imageView?.layer.cornerRadius = imageCornerRadius
        imageView?.layer.borderWidth = imageBorderWidth
    }
    
}

なお、paddingに関してtop, left, right, bottomと別々の値を使用したい場合は、privateでUIEdgeInsetsを定義して、 topPadding等をCGFloatで定義し、それぞれのPaddingからInsetsをいじることで似たように設定できます。

まとめ

Paddingをつけながら画像をaspectFillやaspectFitでボタンのサイズに追随して表示するUIButtonのカスタムクラスをご紹介しました。 ぜひ使ってみてください。

参考

2019年6月振り返り

6月の振り返り

  目標の進捗状況の定期確認です。  

体の基礎づくり

ベンチプレスの重量が徐々に上がっています。100kgまであと少しというところ。

アウトプット

ブログ投稿

  • 目標: 100記事 / 年
  • 実績: 今月 - 本記事も合わせて5記事 / 通年 - 27記事

一月4-5記事くらいが無理しない妥当なところではないかなという気がしてきました。 目標を50記事/年にすることを検討中です。  

登壇

  • 目標: 6件 / 年 
  • 実績: 今月 - 登壇なし / 通年 - 全1件

特に活動できていません。

サービス開発(アプリに限定しない )

  • 目標: 3件
  • 実績: 今月 - 0件 / 通年 - 1件

GenerambaのテンプレートにMVCも追加しました。 新規サービスではないためカウントとしてはそのままです。 GitHub - HironobuIga/GenerambaTemplates: Repository for generamba template samples.

スキルアップ

AtCoder

  • 目標: 水色
  • 実績: 茶色

動けていません。

LeetCode

  • 目標 50問
  • 実績 今月 - 0問 / 通年 - 0問

こちらも動けずです。

Git

  • 目標: 500 commit / 年
  • 実績: 35 commit / 月
  • 実績: 188commit / 年

月次目標に届かずです。 7月はもっと手を動かしていけたらと思います。

その他

  • 5月まで所属していた企業を退職しました。(退職エントリーは書きません)
  • 個人事業主となりフリーランスエンジニアとして活動を開始しました。

7月も頑張ります。

【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()
    }
    
    public override func awakeFromNib() {
        super.awakeFromNib()
        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 required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    public override func awakeFromNib() {
        super.awakeFromNib()
        commonInit()
    }
    
    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】UITabBarの真ん中のボタンが特殊な見た目・挙動のUIを作成する方法

はじめに

InstagramのようにTabBarの真ん中が通常のTabBarItemではなく、押下することでModalで画面が表示されたり、 シートで選択肢が表示されるようなアプリがあります。また真ん中のボタンのデザインがTabBarからはみ出したりしているようなものもあります。このようなUIを作成する方法に関して記載します。

f:id:Iganin:20190616174521p:plain
Instagramサンプル画像

環境設定

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

  • Xcode10.2.1
  • Swift 5.0.1

方法

おそらく一番簡単な方法は真ん中にタップ不可能なTabを追加し、その上にボタンを配置する方法です。 まずはTabBarItemの生成部分を見てみます。ここで注意すべき点は以下です。

  • 真ん中にImage, SelectedImage, titleが全てないUITabBarItemを追加する
  • 真ん中のUITabBarItemのisEnableをfalseにする

このようにすることでTabBarの真ん中にボタン配置用のスペースができ、誤って真ん中のタブを選択されるようなことを防げます。

// タブ情報定義部分
    enum TabType: Int, CaseIterable {
        case pickup
        case news
        case empty
        case friends
        case settings

        var data: (title: String, image: UIImage?, selectedImage: UIImage?) {
            switch self {
            case .pickup :
                return (title: "ピックアップ", image: UIImage(named: "ic_pickup"), selectedImage: UIImage(named: "ic_pickup"))
            case .news:
                return (title: "ニュース", image: UIImage(named: "ic_news"), selectedImage: UIImage(named: "ic_news"))
            case .empty:
                return (title: "", image: nil, selectedImage: nil)
            case .friends:
                return (title: "ともだち", image: UIImage(named: "ic_friends"), selectedImage: UIImage(named: "ic_friends"))
            case .settings:
            return (title: "設定", image: UIImage(named: "ic_setting"), selectedImage: UIImage(named: "ic_setting"))
            }
        }
    }


// タブ生成部分
        var addingViewControllers = [UIViewController]()
        
        TabType.allCases.forEach { type in
            var viewController: UIViewController
            switch type {
            case .pickup: viewController = UIViewController(nibName: nil, bundle: nil)
            case .news: viewController = UIViewController(nibName: nil, bundle: nil)
            case .empty: viewController = UIViewController(nibName: nil, bundle: nil)
            case .friends: viewController = UIViewController(nibName: nil, bundle: nil)
            case .settings: viewController = UIViewController(nibName: nil, bundle: nil)
            }
            let navigationController = UINavigationController(rootViewController: viewController)
            let tabBarItem = UITabBarItem(title: type.data.title, image: type.data.image, selectedImage: type.data.selectedImage)
            if case .empty = type { tabBarItem.isEnabled = false } // 真ん中のタブはさわれない
            navigationController.tabBarItem = tabBarItem
            addingViewControllers.append(navigationController)
        }
        setViewControllers(addingViewControllers, animated: false)

次にTabBarの真ん中のボタン生成・配置をみていきます。 特に特別なことはしていなく、Buttonを作成、Propertyとして保持、Viewに追加、viewWillLayoutSubviewsにてレイアウトの調整を行っています。

AutoLayoutで配置の調整を試みましたがうまくいかなかったため、FrameLayoutで行っています。このあたり良い方法をご存知の方がいましたら共有いただけますと嬉しいです。

    // MARK: Property
    private var centerButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        addCenterButton()
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        centerButton.center = tabBar.center
        centerButton.frame.origin.y = tabBar.frame.origin.y
    }

    func addCenterButton() {
        let centerButton = UIButton(type: .custom)
        centerButton.setBackgroundImage(R.image.white_circle()!, for: .normal)
        centerButton.setImage(R.image.ic_camera()!, for: .normal)
        centerButton.addTarget(self, action: #selector(didTapCenterButton(_:)), for: .touchUpInside)
        centerButton.frame = CGRect(x: 0.0, y: 0.0, width: 50.0, height: 50.0)
        self.centerButton = centerButton
        view.addSubview(centerButton)
    }

    // MARK: Button Action
    @objc func didTapCenterButton(_ sender: UIButton) {
        // 省略
    }

以上で以下のようなTabBarが作成できます。 真ん中のボタンを押下した際の挙動は @objc func didTapCenterButton(_ sender: UIButton) に記載します。

f:id:Iganin:20190616175408p:plain
TabBarサンプル

まとめ

TabBarの真ん中に通常のTabBarとは違う挙動をするボタンを追加する方法を記載しました。 アプリ開発をするなかで要件として出てきた際にはぜひ活用してみてください。 なお、本記事ではコードベースでの生成方法を記載していますが、StoryboardにてTabBarを配置しそちらでレイアウト調整等するのも方法としてありだと思います。

参考

2019年5月振り返り

5月の振り返り

  目標の進捗状況の定期確認です。  

体の基礎づくり

少し重量が上がりました。 デッドリフトで疲れづらくなってきたので、この調子でいけばもう少しあげられる気がします。

アウトプット

ブログ投稿

  • 目標: 100記事 / 年
  • 実績: 今月 - 本記事も合わせて3記事 / 通年 - 22記事

雲行きが怪しいです。  

登壇

  • 目標: 6件 / 年 
  • 実績: 今月 - 登壇なし / 通年 - 全1件

来月から3ヶ月ほど余裕がないため、その間に貯めた知見をそれ以降に出していく感じになりそうです。

サービス開発(アプリに限定しない )

  • 目標: 3件
  • 実績: 今月 - 1件 / 通年 - 1件

含めるか非常に迷いましたが、Generamba用のMVVMテンプレートを作成して公開しました。 現状はMVVMのみですが、Viper、 MVPといった他のアーキテクチャに関しても含めていきたいと考えています。

GitHub - HironobuIga/GenerambaTemplates: Repository for generamba template samples.

スキルアップ

AtCoder

  • 目標: 水色
  • 実績: 茶色

動けていません。

LeetCode

  • 目標 50問
  • 実績 今月 - 0問 / 通年 - 0問

こちらも動けずです。

Git

  • 目標: 500 commit / 年
  • 実績: 44 commit / 月
  • 実績: 153commit / 年

月次目標を超えました。 ただ 1 ~ 4月の間のビハインドが残っているためこの調子で頑張る必要があります。

その他

  • FitBitを買いました。睡眠の計測はなかなか面白いです。
  • Creative Selection、 苦しかった時の話をしようか を読み終わりました。
  • 江副浩正を読み始めました。

5月も頑張ります。