Swift TagListViewでタグ入力UI実装

はじめに

今回作りたかったのはこんなありがちなUI

f:id:yoppy0066:20181113015635g:plain:w200

https://github.com/ElaWorkshop/TagListView
このライブラリを使ったら簡単に実装できた。使い方簡単にメモしておく

ハマりポイントはあまりなかったが、タグを追加していったときに高さの取得方法がわからなかったがソース見てたらそれっぽい値が定義されてた。

tagListView.intrinsicContentSize
実装
import UIKit
import TagListView

class ViewController: UIViewController, TagListViewDelegate, UITextFieldDelegate {

    let MARGIN: CGFloat = 10

    let tagListView = TagListView()
    let textField = UITextField()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.setView()
    }
    func setView() {

        view.addSubview(tagListView)
        view.addSubview(textField)

        tagListView.frame = CGRect(x: MARGIN, y: 50, width: view.frame.width-MARGIN*2, height: 0)

        // タグの削除ボタンを有効に
        tagListView.enableRemoveButton = true

        // 今回は削除ボタン押された時の処理を行う
        tagListView.delegate = self

	// タグの見た目を設定
        tagListView.alignment = .left
        tagListView.cornerRadius = 3
        tagListView.textColor = UIColor.black
	tagListView.borderColor = UIColor.lightGray
        tagListView.borderWidth = 1
        tagListView.paddingX = 10
	tagListView.paddingY = 5
        tagListView.textFont = UIFont.systemFont(ofSize: 16)
        tagListView.tagBackgroundColor = UIColor.white

        // タグ削除ボタンの見た目を設定
	tagListView.removeButtonIconSize = 10
        tagListView.removeIconLineColor = UIColor.black

        // テキストフィールドは適当にセット
        textField.delegate = self
	textField.placeholder = "タグを入力してください"
        textField.returnKeyType = UIReturnKeyType.done

        // レイアウト調整
	updateLayout()
    }

    // テキストフィールドの完了ボタンが押されたら
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        if 0 < textField.text!.count {
            // タグを追加
            tagListView.addTag(textField.text!)

            // テキストフィールドをクリアしてレイアウト調整
            textField.text = nil
            updateLayout()
        }
        return true
    }

    // タグ削除ボタンが押された
    func tagRemoveButtonPressed(_ title: String, tagView: TagView, sender: TagListView) {
	// リストからタグ削除
        sender.removeTagView(tagView)
        updateLayout()
    }

    func updateLayout() {
        // タグ全体の高さを取得
        tagListView.frame.size = tagListView.intrinsicContentSize

        textField.frame = CGRect(x: MARGIN, y: tagListView.frame.origin.y + tagListView.frame.height + 5, width: view.frame.width-MARGIN*2, height: 40)
    }
}

以上です

Swift Carthage 使い方メモ

carthage自体のインストールは済んでる前提で、その後のライブラリインストール手順についてメモしておく。

手順はざっくり以下
1. CartfileにレポジトリURL追加
2. carthage update コマンドでライブラリインストール
3. xcodeからライブラリ追加
4. Run Scriptにパスを追加してビルド

1. Cartfileにレポジトリ追加

プロジェクト直下のCartfileに追記するだけ。だいたい以下のような感じ

github "Alamofire/Alamofire"
2. carthage update コマンドでライブラリインストール

自分の場合、iPhoneアプリしかやったときないので毎回以下を実行

$ carthage update --platform iOS
3. xcodeからライブラリ

General > Linked Frameworks and Libraries
の、「Add Other...」から追加したライブラリのパスを選択。
Carthage/Build/iOS/*****.frameworkを指定

4. Run Scriptにパスを追加してビルド

xcodeの「Build phases」を選択して、「Run Script」を追加

shellに以下を追加

/usr/local/bin/carthage copy-frameworks

input filesに以下を追加

$(SRCROOT)/Carthage/Build/iOS/xxxxx.framework

いつもやってる手順はいつもこれでOK
どうでもいいけど自分の中ではcart+hageで「カートハゲ」って覚えてる。以上です

Swift codableなclass(object)のプロパティに変数で動的にアクセスする

あまり応用シーンはなさそうだけど今回やりたかったことは
user["id"]、user["name"]みたいなかんじでプロパティにアクセスしたかった。

やりたかった理由はObjectから特定のプロパティを表示する画面を作りたかった。

こんなクラスがあってプロパティがたくさんあるイメージ

class User {
  var id: Int
  var name: String
  ・・・
}

テーブルビューに表示するのだが、各行で if や switch でハードコードするのも何となく無駄な気がして別の方法を検索。
たまたま、今回はAPIから取得した値をCodableなClassとして扱っていてStackOverflowにいい方法がのってた。

・StackOverflow
https://stackoverflow.com/questions/45209743/how-can-i-use-swift-s-codable-to-encode-into-a-dictionary

こんな感じのextensionを作る

extension Encodable {
    subscript(key: String) -> Any? {
        return dictionary[key]
    }
    var dictionary: [String: Any] {
        return (try? JSONSerialization.jsonObject(with: JSONEncoder().encode(self))) as? [String: Any] ?? [:]
    }
}

利用方法

// Object
var user = User(id: 1, name: "user1", ・・・)

// 表示する順番を定義
var fields = [ "id", "name", ・・・ ]

// テーブル表示
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  // user[fields[indexPath.row]] でプロパティにアクセスできる
}

プロパティの値を更新したい場合はこれだと上手くいかないけど今回は表示するだけの目的だったので事足りた。以上です。

Swift LINEぽいメッセージアプリの入力UIをつくったのでメモ

はじめに

かなり前にこちらでメッセージを表示する部分の吹き出しの作り方をメモした。
http://kimagureneet.hatenablog.com/entry/2015/09/19/005407

今回はメッセージ入力部分のUIの作り込みの方法をメモ。こんな感じのものを作りたかった。
f:id:yoppy0066:20181101222535g:plain:w200

やりたいことは
・入力フォームを画面下に固定
・フォーカスを当てたらキーボードの上に入力フォームを移動
・複数行にも対応したいのでUITextFieldでなくUITextViewを使用

上記を満たすためにやらなくてはならないこと(上と重複するけど)
・キーボードを開いたとき
入力フォームの位置をキーボードの上に移動
UITextViewの高さを複数行見えるように変更

・キーボードを閉じたとき
入力フォームの位置を画面下に移動
UITextViewの高さを1行見えるように変更

ざっくりこんな感じ。あとはキーボードの上に完了ボタンを追加したり細々とした内容。

実装

import UIKit

class ViewController: UIViewController, UITextViewDelegate {

    let MARGIN_MSG: CGFloat = 1
    let HEIGHT_BOX: CGFloat = 50
    let WIDTH_SUBMIT: CGFloat = 70

    let MIN_HEIGHT_MSG: CGFloat = 50
    let MAX_HEIGHT_MSG: CGFloat = 100

    // メッセージを表示する領域
    var tableView: UITableView!

    // メッセージBOX(入力フォームと送信ボタン)
    var box: UIView!
    var msg: UITextView!
    var submit: UIButton!

    // テーブルビューのサイズ情報
    var sizeTable: CGSize!

    // メッセージBOXのフレーム情報(キーボードが閉じられた時の情報)
    var frBox: CGRect!

    // キーボードのフレーム情報
    var frKeyboard: CGRect!

    override func viewDidLoad() {
        super.viewDidLoad()
	setView()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
	let notificationCenter = NotificationCenter.default

        notificationCenter.addObserver(self, selector: #selector(ViewController.handleKeyboardWillShowNotification(_:)), name: .UIKeyboardWillShow, object: nil)
        notificationCenter.addObserver(self, selector: #selector(ViewController.handleKeyboardWillHideNotification(_:)), name: .UIKeyboardWillHide, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: self.view.window)
	NotificationCenter.default.removeObserver(self, name: .UIKeyboardDidHide, object: self.view.window)
    }

    // キーボードが表示されるときに行う処理
    @objc func handleKeyboardWillShowNotification(_ notification: Notification) {
        let userInfo = notification.userInfo!
        frKeyboard = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
        drawBox()
    }

    // キーボードが非表示になるときに行う処理
    @objc func handleKeyboardWillHideNotification(_ notification: Notification) {
        box.frame = frBox
        tableView.frame.size = sizeTable
    }

    // メッセージBOX描画処理
    func drawBox() {
        let statusBarHeight = UIApplication.shared.statusBarFrame.height
        let bound: CGSize = UIScreen.main.bounds.size
        var height = msg.sizeThatFits(CGSize(width: msg.frame.size.width, height: CGFloat.greatestFiniteMagnitude)).height
        if height < MIN_HEIGHT_MSG {
            height = MIN_HEIGHT_MSG
        } else if MAX_HEIGHT_MSG < height {
            height = MAX_HEIGHT_MSG
        }
        // 入力フォームの位置調整
        msg.frame = CGRect(x: msg.frame.origin.x, y: msg.frame.origin.y, width: msg.frame.width, height: height)
        box.frame = CGRect(x: box.frame.origin.x, y: bound.height - frKeyboard.size.height - box.frame.height, width: msg.frame.width, height: height)

        // テーブルビューの高さ調整
        tableView.frame.size = CGSize(width: tableView.frame.width, height: bound.height - (statusBarHeight + frKeyboard.height + height))
    }

    func textViewDidChange(_ textView: UITextView) {
	drawBox()
    }

    func setView() {

        let statusBarHeight = UIApplication.shared.statusBarFrame.height
        let widthMax = view.frame.width
        let heightMax = view.frame.height

        // メッセージを表示する領域
        tableView = UITableView(frame: CGRect(x:0, y:statusBarHeight, width:widthMax, height:heightMax - (statusBarHeight + HEIGHT_BOX)))
        tableView.separatorStyle = .none
        view.addSubview(tableView)

        // 入力フォームと送信ボタンを表示する領域
        box = UIView(frame: CGRect(x: 0, y: tableView.frame.origin.y + tableView.frame.height, width: widthMax, height: HEIGHT_BOX))
	view.addSubview(box)

        // 送信ボタン
        submit = UIButton(frame: CGRect(x: widthMax-(WIDTH_SUBMIT+MARGIN_MSG), y: MARGIN_MSG, width: WIDTH_SUBMIT, height: HEIGHT_BOX-MARGIN_MSG))
        submit.setTitle("送信", for: UIControlState.normal)
        submit.setTitleColor(UIColor.white, for: UIControlState.normal)
        submit.backgroundColor = UIColor.black
	submit.layer.cornerRadius = 2.0
        submit.layer.borderColor = UIColor.lightGray.cgColor
        submit.layer.borderWidth = 3
        box.addSubview(submit)

        // 入力フォーム
	msg = UITextView(frame: CGRect(x: MARGIN_MSG, y: MARGIN_MSG, width: widthMax-(MARGIN_MSG*3+WIDTH_SUBMIT), height: HEIGHT_BOX-MARGIN_MSG))
        msg.font = UIFont.systemFont(ofSize: 18)
        msg.layer.borderColor = UIColor.lightGray.cgColor
        msg.layer.borderWidth = 3
        msg.delegate = self
        box.addSubview(msg)

        // キーボード完了ボタン
        let keyboard = UIView(frame: CGRect(x: 0, y: 0, width: widthMax, height: 40))
        keyboard.backgroundColor = UIColor.lightGray
        let button = UIButton(frame: CGRect(x: widthMax - 50, y: 5, width: 50, height: 30))
        button.setTitle("完了", for: .normal)
        button.layer.cornerRadius = 2.0
        keyboard.addSubview(button)
	msg.inputAccessoryView = keyboard
        button.addTarget(self, action: #selector(onClose(sender:)), for: .touchUpInside)

        // メッセージBOXの位置を保持しておく
	frBox = box.frame

        // テーブルビューのサイズを保持しておく
        sizeTable = tableView.frame.size
    }

    // 完了ボタンでキーボードを閉じる
    @objc func onClose(sender: UIBarButtonItem){
        msg.endEditing(true)
    }

}

ずらずら書いたけど以上です。ちなみにSwift4で確認した。

Swift ImageSlideshowのPagerの色、位置を変更する

https://github.com/zvonicek/ImageSlideshow
こちらのライブラリの話

デフォルトだと画像とページャーガ重なっていて見づらいので、色を変えて画像の下にくるようにする

var slideShow = ImageSlideshow()

let pager = UIPageControl()
pager.pageIndicatorTintColor = UIColor.lightGray
pager.currentPageIndicatorTintColor = UIColor.black
slideShow.pageIndicator = pager
slideShow.pageIndicatorPosition = PageIndicatorPosition(horizontal: .center, vertical: .under)

let imageSources = [ImageSource(imageString: "img1")!, ImageSource(imageString: "img2")!, ImageSource(imageString: "img3")!, ImageSource(imageString: "img4")!]
slideShow.setImageInputs(imageSources)

デフォルトこっちの方がいい気が。。以上です

Swift UITableViewですぐ忘れる細かいことまとめておく

セルの罫線を消す

tableView.separatorStyle = .none

セルの罫線の隙間をなくす

UITableView.appearance().separatorInset = UIEdgeInsets.zero

空のセルは表示しない

tableView.tableFooterView = UIView(frame: .zero)

セルをタップした時の色がかわらないように

cell.selectionStyle = .none


随時追加する予定。以上です

ionic async awaitでAPIへ同期的にリクエスト

やりたいことはタイトルのとおり。初めてでハマったのでコードをメモしておく。

import { Component } from '@angular/core';
import { Http, Headers,RequestOptions } from '@angular/http'

import 'rxjs/add/operator/map'
import 'rxjs/add/operator/catch'

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  constructor(
    public http: Http
  ) {
  }

  ionViewDidEnter() {
    this.requestApiAsync()
  }

  async requestApiAsync() {
    for (var i = 1; i <= 3; i++) {
      console.log(i + '回目')
      let result = await this.requestApi()
      console.log(result)
    }
  }

  requestApi() {
    return new Promise((resolve, reject) => {
      let url = 'http://example.com/path/to'
      let body = 'key=value'

      let headers = new Headers({
	"Content-Type": "application/x-www-form-urlencoded"
      })

      let options = new RequestOptions({
	headers: headers
      })

      this.http.post(url, body, options)
	.map(response => response).catch(error => error)
	.subscribe(
          result => resolve(result.json()),
          error => reject(error)
	)
    })
  }
}

以上です