20200928のiOSに関する記事は15件です。

[Xcode12.0.1]SpriteKitで全画面表示する

今回久々にSpriteKitに触れる機会があり、開始早々ハマりポイントがあったので備忘もかねて残しておきます。

解決したかった課題

対応前 対応後
スクリーンショット 2020-09-28 22.38.31.png スクリーンショット 2020-09-28 22.46.52.png

対応前のスクショの赤矢印のように、画面中央にあるSKViewの範囲を対応後のように画面全体に広げたかった。

解決方法

色々調べた割に、LaunchScreenFileを指定するだけという対応で呆気なく解決しました。。。
スクリーンショット 2020-09-28 22.44.35.png

試行錯誤

折角なので、どんな感じで翻弄されていたかという恥ずかしい部分もさらけ出そうかと思います。

  1. AutoLayoutの問題では?と思ってGameViewControllerのルートビューをUIViewに変更し、そこにSKViewを制約つけて貼る → ?‍♀️
  2. サイズを指定しようと思い、SKViewのFrameサイズを上書きする → ?‍♀️
  3. ビューデバッガを使って確認したところ、ルートがUIWindowSceneとなっていたので、もしやSceneDelegateかな?と思いSceneDelegateを追加する → ?‍♀️ スクリーンショット 2020-09-28 23.11.43.png
  4. 色々ググって、やっとLaunchScreenFileに気づく → ?‍♂️

これで1時間近く浪費してしまいました。。。
これからは己を過信せず、最初からググります。反省。。。

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

別画面に値を渡す方法について

画面遷移時に値を渡す

画面遷移時に遷移先の変数varに値を渡すことで、別の画面に値を渡すことができます。

ViewController
let secondViewController = self.storyboard?.instantiateViewController(withIdentifier: "second") as! secondViewController
secondViewController. value = "value"
self.present(secondViewController, animated: true, completion: nil)
secondViewController
class secondViewController: UIViewController {

    var value = String()

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

画面遷移時以外で値を渡す

tabBarControllerは、最初にタブを切り替えた時に画面が生成され、削除されないため
画面遷移時以外に値を渡す方法が必要になります。

プロトコルを使用して、特定の処理後に実行させることで、別の画面に値を渡したり処理を行うことができます。

ViewController
var process: anotherScreenProcess!

class ViewController: UIViewController {

     override func viewDidLoad() {
          super.viewDidLoad()

          self.process = secondViewController() as anotherScreenProcess
          self.process.sendValue()
     }
}
secondViewController
protocol anotherScreenProcess {
    func sendValue()
}

class secondViewController: UIViewController, anotherScreenProcess {
     override func viewDidLoad() {
          super.viewDidLoad()
     }

     // 別画面からの処理
     func sendValue() {

     }
}

tabBarControllerについて

tabBarControllerは最初の画面遷移時のみ画面が作成され、削除されません。
そのため、プロトコルの使用以外に作成したtabBarControllerを削除することで、
画面自体の再作成を行うことができます。

self.tabBarController!.viewControllers![1].dismiss(animated: true, completion: nil)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】UICollectionViewでInstagramのプロフィールっぽいUIをコードだけで実装してみた without storyboard

はじめに

今回はUICollectionViewでInstagramのプロフィールっぽいUIをコードだけで実装してみる。
という内容です

この記事が誰かの役に立てば幸いです

ただただSwift初心者がUIを真似して作っただけなので、何かと至らない点があるかと思いますが、
コードの書き方、間違い等、お気づきのところあれば是非アドバイスくださると助かります!!

書き始めたの3ヶ月前、、ピエンパオンが止まりません

完成形

last_qiita.gif

[めっちゃ起業家に憧れるインスタグラマー]

対象読者

・ iOSアプリ開発初心者の方
・ UICollectionViewの使い方を知りたい方
・ StoryBoardを使用せずに開発してみたい方
・ InstagramのUIが好きな方

開発環境

・ Version 11.3 (11C29)
・ Swift 5

完成版 Github

以下にソースコードを載せておきます

https://github.com/Isseymiyamoto/FamousAppUI/tree/master/Instagram_profile/Instagram_profile

ファイル構成

今回、データの取得等の通信は行わないためViewControllerフォルダ内に新しいファイルを追加していきます
Utils > Extensions.swiftではLayout関連の処理を簡素化するための関数を入れていますが、
こちらの記事では詳細を記述しないので、Githubよりコピペしてくださると助かります

スクリーンショット 2020-09-17 15.46.20.png

さて、実装に移りましょう

実装の手順

1、2に関しては、TabBarが必要なければスキップしてください

  1. 利用するcontrollerファイルの作成
  2. UITabBarControllerを用いて、TabBarと表示するControllerの決定
  3. SceneDelegate.swiftにて、起動時に表示するControllerの決定
  4. Viewフォルダにて、ProfileHeaderCellの作成
  5. Viewフォルダにて、FilterViewの作成
  6. Viewフォルダにて、投稿写真表示用のPostCellの作成
  7. 合体 and 完成!!

1.利用するControllerファイルの作成

ここでは、2で実装するTabBarと連携するためのControllerファイルを作成しましょう
Instagramでは表示するタブアイコンが5つありますので、5つのファイルをControllerフォルダ直下に作成します

ProfileController.swift
import UIKit

class ProfileController: UICollectionViewController{

  override func viewDidLoad() {
    super.viewDidLoad()

  }

}

その他の4ファイルについては以下で結構です

FeedController.swift
import UIKit

class FeedController: UIViewController{

  override func viewDidLoad() {
    super.viewDidLoad()

    // navigationBarに表示したい文字列を入れましょう
    navigation.item = "投稿"
  }

}

2.UITabBarControllerを用いて、TabBarと表示するControllerの決定

1にて作成したファイルをTabBarと連携させるために、controllerフォルダ直下にMainTabController.swiftを作成します

MainTabController.swift
import UIKit

class MainTabController: UITabBarController{

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()

        configureUI()
        configureViewControllers()
    }

    // MARK: - Helpers

    func configureUI(){
        view.backgroundColor = .white
        tabBar.tintColor = .black
    }

    func configureViewControllers(){

        let feed = FeedController()
        let nav1 = templateNavigationController(image: UIImage(systemName: "house"), rootViewController: feed)

        let search = SearchController()
        let nav2 = templateNavigationController(image: UIImage(systemName: "magnifyingglass"), rootViewController: search)

        let upload = UploadPostController()
        let nav3 = templateNavigationController(image: UIImage(systemName: "plus.app"), rootViewController: upload)

        let notification = NotificationController()
        let nav4 = templateNavigationController(image: UIImage(systemName: "heart"), rootViewController: notification)
        // 2件通知きてるかのように表示
        nav4.tabBarItem.selectedImage = UIImage(systemName: "heart.fill")
        nav4.tabBarItem.badgeValue = "2"

        let profile = ProfileController(collectionViewLayout: UICollectionViewFlowLayout())
        let nav5 = templateNavigationController(image: UIImage(systemName: "person"), rootViewController: profile)

        // tabバーに配置するControllerを決定
        viewControllers = [nav1, nav2, nav3, nav4, nav5]

        // profileControllerを初期表示
        selectedIndex = 4
    }

    // 任意のrootViewController、tabIconイメージを設定する関数, configureViewControllers内で使用
    func templateNavigationController(image: UIImage?, rootViewController: UIViewController) -> UINavigationController{
        let nav = UINavigationController(rootViewController: rootViewController)
        nav.tabBarItem.image = image
        nav.navigationBar.tintColor = .white
        return nav
    }
}

3.SceneDelegate.swiftにて、起動時に表示するControllerの決定

さて、SceneDelegate.swiftを編集して、MainTabControllerを起動時に表示するように設定しましょう

SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

  // 省略

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = scene as? UIWindowScene else { return }
        window = UIWindow(windowScene: scene)
        window?.rootViewController = MainTabController()
        window?.makeKeyAndVisible()
    }

  // 省略
}

こちら設定後Simulatorを立ち上げると以下のように表示されれば完璧です
スクリーンショット 2020-09-28 20.04.35.png

4.Viewフォルダにて、ProfileHeaderCellの作成

次に、ProfileControllerに適用するViewの作成に入ります
以下のような形式で作っていきますが、まずここでプロフィール概要部分のProfileHeaderCellを作りましょう

スクリーンショット 2020-09-28 20.17.06.png

View直下にProfileHeader.swiftをファイルを作成します

ProfileHeader.swift
import UIKit

// ハリボテなので無くても良い
protocol ProfileHeaderDelegate: class {
    func handleEditProfile(_ header: ProfileHeader)
}

class ProfileHeader: UICollectionViewCell{

    // MARK: - Properties

    // ハリボテなので無くても良い
    weak var delegate: ProfileHeaderDelegate?

    private let profileImageView: UIImageView = {
        let iv = UIImageView()
        iv.contentMode = .scaleAspectFit
        iv.clipsToBounds = true
        iv.image = UIImage(named: "適当に写真入れてみてください")
        iv.layer.borderColor = UIColor.black.cgColor
        return iv
    }()

    private lazy var postCountButton = makeStatsButton(withNumber: "12")
    private lazy var followingCountButton = makeStatsButton(withNumber: "320")
    private lazy var followerCountButton = makeStatsButton(withNumber: "1000")

    private lazy var postCountLabel = makeStatsTitle(withTitle: "投稿")
    private lazy var followingCountLabel = makeStatsTitle(withTitle: "フォロー中")
    private lazy var followerCountLabel = makeStatsTitle(withTitle: "フォロワー")

    private let fullnameLabel: UILabel = {
        let label = UILabel()
        label.text = "オナマエー"
        label.font = UIFont.boldSystemFont(ofSize: 14)
        return label
    }()

    private let bioLabel: UILabel = {
        let label = UILabel()
        label.text = "これはInstagramのプロフィールのUIをひたすらに真似する試みです。そうです。ただただ真似るだけです。"
        label.font = UIFont.systemFont(ofSize: 14)
        label.numberOfLines = 3
        return label
    }()

    private let editProfileButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("プロフィールを編集", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.addTarget(self, action: #selector(handleEditProfileButtonTapped), for: .touchUpInside)
        button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 14)
        button.layer.borderColor = UIColor.lightGray.cgColor
        button.layer.borderWidth = 1
        button.layer.cornerRadius = 4
        button.backgroundColor = .white
        return button
    }()

    private let storiesPlusButton: UIButton = {
        let button = UIButton(type: .system)
        button.setImage(UIImage(systemName: "plus"), for: .normal)
        button.tintColor = .black
        button.backgroundColor = .clear
        button.layer.borderColor = UIColor.lightGray.cgColor
        button.layer.borderWidth = 0.75
        return button
    }()

    private let storiesPlusLabel: UILabel = {
        let label = UILabel()
        label.text = "新規"
        label.textAlignment = .center
        label.font = UIFont.systemFont(ofSize: 10)
        return label
    }()

    // MARK: - Lifecycle

    override init(frame: CGRect) {
        super.init(frame: frame)

        backgroundColor = .systemGroupedBackground

        let postCountStack = makeStatsStackView(button: postCountButton, label: postCountLabel)
        let followingCountStack = makeStatsStackView(button: followingCountButton, label: followingCountLabel)
        let followerCountStack = makeStatsStackView(button: followerCountButton, label: followerCountLabel)

        let infoStack = UIStackView(arrangedSubviews: [postCountStack, followingCountStack, followerCountStack])
        infoStack.axis = .horizontal
        infoStack.alignment = .center
        infoStack.distribution = .fillEqually

        addSubview(profileImageView)
        profileImageView.anchor(top: safeAreaLayoutGuide.topAnchor, left: leftAnchor, paddingTop: 16, paddingLeft: 16)
        profileImageView.setDimensions(width: 96, height: 96)
        profileImageView.layer.cornerRadius = 96 / 2

        addSubview(infoStack)
        infoStack.centerY(inView: profileImageView)
        infoStack.anchor(left: profileImageView.rightAnchor, right: rightAnchor, paddingLeft: 16, paddingRight: 32)

        addSubview(fullnameLabel)
        fullnameLabel.anchor(top: profileImageView.bottomAnchor, left: leftAnchor, right: rightAnchor, paddingTop: 16, paddingLeft: 16, paddingRight: 16)

        addSubview(bioLabel)
        bioLabel.anchor(top: fullnameLabel.bottomAnchor, left: leftAnchor, right: rightAnchor, paddingTop: 4, paddingLeft: 16, paddingRight: 16)

        addSubview(editProfileButton)
        editProfileButton.anchor(top: bioLabel.bottomAnchor, left: leftAnchor, right: rightAnchor, paddingTop: 16, paddingLeft: 16, paddingRight: 16 )

        addSubview(storiesPlusButton)
        storiesPlusButton.anchor(top: editProfileButton.bottomAnchor, left: leftAnchor, paddingTop: 16, paddingLeft: 16)
        storiesPlusButton.setDimensions(width: 64, height: 64)
        storiesPlusButton.layer.cornerRadius = 64 / 2

        addSubview(storiesPlusLabel)
        storiesPlusLabel.centerX(inView: storiesPlusButton)
        storiesPlusLabel.anchor(top: storiesPlusButton.bottomAnchor, paddingTop: 4)
    }



    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: - Selectors

    // ハリボテなので無くても良い
    @objc func handleEditProfileButtonTapped(){
        delegate?.handleEditProfile(self)
    }


    // MARK: - Helpers

    // ボタンと詳細数を縦並びに揃えるStackView作成用
    fileprivate func makeStatsStackView(button: UIButton, label: UILabel) -> UIStackView{
        let stack = UIStackView(arrangedSubviews: [button, label])
        stack.axis = .vertical
        stack.alignment = .center
        stack.setDimensions(width: 160, height: 40)
        return stack
    }

    // 投稿数やフォロワー等の表示ボタン作成用
    private func makeStatsButton(withNumber number: String) -> UIButton{
        let button = UIButton(type: .system)
        button.setTitle(number, for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
        return button
    }
    // 投稿数やフォロワー等の詳細数表示ラベル作成用   
    private func makeStatsTitle(withTitle title: String) -> UILabel{
        let label = UILabel()
        label.text = title
        label.textAlignment = .center
        label.font = UIFont.systemFont(ofSize: 14)
        return label
    }
}

こちらをProfileControllerに適用してみましょう(5, 6では一旦すっ飛ばします)

ProfileController.swift
import UIKit

private let profileHeaderCell = "ProfileHeaderCell"

class ProfileController: UICollectionViewController{

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()

        configureUI()
    }

    // MARK: - Selectors

    // お飾り
    @objc func handleRightButtonTapped(){
        print("DEBUG: you pressed the button..")
    }

    @objc func handleRefresh(){
        // データがないので何もしません
        collectionView.refreshControl?.beginRefreshing()
        collectionView.refreshControl?.endRefreshing()
    }

    // MARK: - Helpers

    // 全体UIの設定
    func configureUI(){
        view.backgroundColor = .systemGroupedBackground

        configureNavigationBar()
        configureCollectionView()

        // 下にスワイプしてリロードしてる風設定
        let refreshControl = UIRefreshControl()
        refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
        collectionView.refreshControl = refreshControl
    }

    // navigationBarに関する諸設定
    func configureNavigationBar(){
        navigationController?.navigationBar.tintColor = .black
        navigationController?.navigationBar.barTintColor = .systemGroupedBackground
        navigationController?.navigationBar.isTranslucent = false
        navigationController?.navigationBar.shadowImage = UIImage()

        navigationItem.title = "user_id"
        navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "line.horizontal.3"), style: .plain, target: self, action: #selector(handleRightButtonTapped))
    }

    func configureCollectionView(){
        collectionView.backgroundColor = .systemGroupedBackground

        // ProfileHeaderの登録
        collectionView.register(ProfileHeader.self, forCellWithReuseIdentifier: profileHeaderCell)

        // collectionViewをtabBarにかからないように配置
        guard let tabHeight = tabBarController?.tabBar.frame.height else { return }
        collectionView.contentInset.bottom = tabHeight
    }
}

// MARK: - UICollectionViewDataSource / UICollectionViewDelegate

extension ProfileController{

    // セクション数はひとまず1に設定 → PostCell設定後2に変更
    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    // セクション内に表示するセルの数 → ProfileHeaderは1つで良いので一旦1
    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 1
    }

    // 表示するcellの設定
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: profileHeaderCell, for: indexPath) as! ProfileHeader
        return cell
    }
}

// MARK: - UICollectionViewDelegateFlowLayout

extension ProfileController: UICollectionViewDelegateFlowLayout{

    // cellのサイズ設定 → 適当に高さは変えてください
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        return CGSize(width: view.frame.width, height: 340)
    }
}

ここまででSimulatorを立ち上げるとProfileHeader箇所は表示されましたでしょう!多分!
ここからサクッと最後まで終わらせましょう

5.Viewフォルダにて、FilterViewの作成

FilterViewは、自分の投稿一覧or友達がタグ付けした一覧を表示するためのフィルター部分です
ProfileController上では、indexPath.section = 1 のheaderとして表示します

さて、FilterViewですが、UICollectionReusableViewの中にUICollectionViewを設置する形で作成します
はい、つまりUICollectionViewCellのFilterViewCellも別ファイルで作ります。頑張りましょう

ProfileFilterView.swift
import UIKit

private let profileHeaderCellIdentifier = "profileHeaderCell"

// ハリボテ
protocol ProfileFilterViewDelegate: class {
    func filterView(_ view: ProfileFilterView, didSelect index: Int)
}

class ProfileFilterView: UICollectionReusableView {

    // MARK: - Properties

    // ハリボテ
    weak var delegate: ProfileFilterViewDelegate?

    // viewに載せていくcollectionView
    lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.backgroundColor = .systemGroupedBackground
        cv.delegate = self
        cv.dataSource = self
        return cv
    }()

    // こいつをアニメーションさせていい感じに選択した感を演出
    private let underlineView: UIView = {
        let view = UIView()
        view.backgroundColor = .black
        return view
    }()

    // profileHeaderCellとの境界線
    private let abovelineView: UIView = {
        let view = UIView()
        view.backgroundColor = .lightGray
        return view
    }()

    // MARK: - Lifecycle

    override init(frame: CGRect) {
        super.init(frame: frame)

        collectionView.register(ProfileFilterCell.self, forCellWithReuseIdentifier: identifier)

        // 初期化時にisSelected = trueにするcellを決定する
        let selectedIndexPath = IndexPath(row: 0, section: 0)
        collectionView.selectItem(at: selectedIndexPath, animated: true, scrollPosition: .left)

        addSubview(collectionView)
        // 親viewいっぱいにcollectionViewを広げる
        collectionView.addConstraintsToFillView(self)
    }

    override func layoutSubviews() {
        addSubview(abovelineView)
        abovelineView.anchor(left: leftAnchor, bottom: topAnchor, width: frame.width, height: 0.5)

        addSubview(underlineView)
        underlineView.anchor(left: leftAnchor, bottom: bottomAnchor, width: frame.width / 2, height: 1)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

// MARK: - UICollectionViewDataSource

extension ProfileFilterView: UICollectionViewDataSource{
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // tag or post の 2択なので return 2 でも ok
        return ProfileFilterOptions.allCases.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! ProfileFilterCell

        // cell側のoptionを更新
        let option = ProfileFilterOptions(rawValue: indexPath.row)
        cell.option = option

        return cell
    }
}

// MARK: - UICollectionViewDelegate

extension ProfileFilterView: UICollectionViewDelegate{
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let cell = collectionView.cellForItem(at: indexPath)
        // underlineViewをtouchUpInsideされたcellのx座標に0.3秒で移動させる
        let xPosition = cell?.frame.origin.x ?? 0

        UIView.animate(withDuration: 0.3) {
            self.underlineView.frame.origin.x = xPosition
        }

        // ハリボテ → 本来ProfileControllerにて表示画像変更できるように処理書く
        delegate?.filterView(self, didSelect: indexPath.row)
    }
}

// MARK: - UICollectionViewDelegateFlowLayout

extension ProfileFilterView: UICollectionViewDelegateFlowLayout{
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let count = CGFloat(ProfileFilterOptions.allCases.count)
        return CGSize(width: frame.width / count, height: frame.height)
    }

    // item同士の隙間がないよう設置
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }
}
ProfileFilterViewCell.swift
import UIKit

// 投稿 or tag付け投稿一覧どっちやねんを見極めます
enum ProfileFilterOptions: Int, CaseIterable{
    case post
    case tag

    var systemImage: UIImage? {
        switch self {
        case .post: return UIImage(systemName: "rectangle.split.3x3")
        case .tag: return UIImage(systemName: "person.crop.rectangle")
        }
    }
}

class ProfileFilterViewCell: UICollectionViewCell{

    // MARK: - Properties

    // 投稿 or tag付け投稿一覧どっちやねんが更新されたら、imageViewのimageを変更するように設定
    var option: ProfileFilterOptions! {
        didSet{ imageView.image = option.systemImage }
    }

    private var imageView: UIImageView = {
        let iv = UIImageView()
        return iv
    }()

    // 選択された場合と否かでtintColor変更
    override var isSelected: Bool {
        didSet{
            imageView.tintColor = isSelected ? .black : .lightGray
        }
    }

    // MARK: - Lifecycle

    override init(frame: CGRect) {
        super.init(frame: frame)

        addSubview(imageView)
        imageView.tintColor = .lightGray
        imageView.setDimensions(width: 24, height: 24)
        imageView.center(inView: self)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

さあ、そろそろ記事書くのに息切れしてきましたが、工程6に進みましょう

6.Viewフォルダにて、投稿写真表示用のPostCellの作成

さあ、最後にただただ写真を表示するだけのcellをサクッと作りましょう!!

PostCell.swift
import UIKit

class PostCell: UICollectionViewCell{

    // MARK: - Properties

    let postImageView: UIImageView = {
        let iv = UIImageView()
        iv.contentMode = .scaleAspectFill
        iv.clipsToBounds = true
        return iv
    }()


    // MARK: - Lifecycle

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.layer.borderColor = UIColor.white.cgColor
        self.layer.borderWidth = 0.5

        addSubview(postImageView)
        postImageView.addConstraintsToFillView(self)
        postImageView.center(inView: self)

    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

一気に人生イージーモードに突入したので、本当に最後にProfileControllerにて全部合致しましょう!!

7.合体 and 完成!!

ProfileController.swift
import UIKit

private let filterViewIdentifier = "filterView"
private let profileHeaderCellIdentifier = "profileHeaderCell"
private let postCellIdentifier = "postCell"

class ProfileController: UICollectionViewController{

    // MARK: - Properties

    // post cell箇所に適応したいハリボテUIIMage配列を作成
    private var imageArray: [UIImage?] =
        [UIImage(named: "jeff"), UIImage(named: "zack"), UIImage(named: "elon"), UIImage(named: "steve"),
         UIImage(named: "jeff"), UIImage(named: "zack"), UIImage(named: "elon"), UIImage(named: "steve"),
         UIImage(named: "jeff"), UIImage(named: "zack"), UIImage(named: "elon"), UIImage(named: "steve"),
         UIImage(named: "jeff"), UIImage(named: "zack"), UIImage(named: "elon"), UIImage(named: "steve")]


    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()

        configureUI()
    }


    // MARK: - Selectors

    @objc func handleRightButtonTapped(){
        print("DEBUG: you pressed the button..")
    }

    @objc func handleRefresh(){
        // データがないので何もしません
        collectionView.refreshControl?.beginRefreshing()
        collectionView.refreshControl?.endRefreshing()
    }

    // MARK: - Helpers

    func configureUI(){
        view.backgroundColor = .systemGroupedBackground

        configureNavigationBar()
        configureCollectionView()

        let refreshControl = UIRefreshControl()
        refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
        collectionView.refreshControl = refreshControl
    }


    func configureNavigationBar(){
        navigationController?.navigationBar.tintColor = .black
        navigationController?.navigationBar.barTintColor = .systemGroupedBackground
        navigationController?.navigationBar.isTranslucent = false
        navigationController?.navigationBar.shadowImage = UIImage()

        navigationItem.title = "user_id"
        navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "line.horizontal.3"), style: .plain, target: self, action: #selector(handleRightButtonTapped))

    }

    func configureCollectionView(){
        collectionView.backgroundColor = .systemGroupedBackground

        collectionView.register(ProfileHeader.self, forCellWithReuseIdentifier: profileHeaderCellIdentifier)
        collectionView.register(PostCell.self, forCellWithReuseIdentifier: postCellIdentifier)

        collectionView.register(ProfileFilterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: filterViewIdentifier)

        guard let tabHeight = tabBarController?.tabBar.frame.height else { return }
        collectionView.contentInset.bottom = tabHeight

        // スクロールした際にFilterViewをnavigationBarと同化させる
        guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
        flowLayout.sectionHeadersPinToVisibleBounds = true
    }
}

// MARK: - UICollectionViewDataSource

extension ProfileController{

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 2
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        switch section {
        case 0:
            return 1
        default:
            // 表示したいimage数だけcellを配置
            return imageArray.count
        }
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        switch indexPath.section {
        case 0:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: profileHeaderCellIdentifier, for: indexPath) as! ProfileHeader
            return cell
        default:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: postCellIdentifier, for: indexPath) as! PostCell
            // cellのimageに代入
            cell.postImageView.image = imageArray[indexPath.row]
            return cell
        }
    }

    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        // headerとしてProfileFilterView登録
        let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: filterViewIdentifier, for: indexPath) as! ProfileFilterView
        return header
    }
}

// MARK: - UICollectionViewDelegate

extension ProfileController{
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        if(indexPath.section == 1){
          print("DEBUG: this item is \(indexPath.row)")
        }
    }
}
// MARK: - UICollectionViewDelegateFlowLayout

extension ProfileController: UICollectionViewDelegateFlowLayout{

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        switch section {
        case 0:
            return CGSize(width: 0, height: 0)
        default:
            let height: CGFloat = 50
            return CGSize(width: view.frame.width, height: height)
        }
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        switch indexPath.section {
        case 0:
            let height: CGFloat = 340
            return CGSize(width: view.frame.width, height: height)
        default:
            // 3列表示、正方形サイズに
            let size = view.frame.width / 3
            return CGSize(width: size, height: size)
        }
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }
}

最後に

どうでしょうか?
いい感じに、InstagramのプロフィールっぽいハリボテUI完成しましたでしょうか?

最後の方、段々疲れて来て口数減ってしまいました。
説明が足りない箇所等コメントにて教えてくださると追記したいと思います。

というか、コピペしても動かんねんけどという苦情あったらすみません。すぐ直します。

それでは!

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

【iOS】iOS14でcanOpenURLがfalseになる

検証環境

Xcode: 12.0.1
iOS: 14.0
Swift: 5

事象

下記のコードでブラウザを外部起動しようとしたら、canOpenURLメソッドがfalseになり、外部起動されなかった。

guard let url: URL = URL(string: "https://www.yahoo.co.jp/") else { return }
if UIApplication.shared.canOpenURL(url) {
    UIApplication.shared.open(url, options: [:], completionHandler: nil)
}

コンソールを見ると下記のようなエラーメッセージが表示された。
httpsのURLスキームは許可されていないとのこと。

-canOpenURL: failed for URL: "https://www.yahoo.co.jp/" - error: "This app is not allowed to query for scheme https"

対処法

エラーメッセージに書いてある通り、httpsは許可されていないので許可してあげれば良い。

canOpenUrlの公式ドキュメントを確認すると、info.plistにLSApplicationQueriesSchemesを追加して、そこに許可するスキームを定義してあげれば良さそう。
(下記引用)

If your app is linked on or after iOS 9.0, you must declare the URL schemes you pass to this method by adding the LSApplicationQueriesSchemes key to your app's Info.plist file. This method always returns false for undeclared schemes, whether or not an appropriate app is installed. To learn more about the key, see LSApplicationQueriesSchemes.

LSApplicationQueriesSchemesのドキュメントにもアプリがcanOpenURL:メソッドを使用してテストできるURLスキームを指定すると書いてある。

実装

下記のように、LSApplicationQueriesSchemesにhttpsのスキームを定義する。
スクリーンショット_2020-09-28_17_33_02.png

この状態で実行すると、無事canOpenURLにtrueが返される。
以上。

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

[機械学習]Create MLでObject Detectionを試してみた[物体検知]

今回実装してみたもの

ダウンロード.gif

準備

今回は冒頭のGifの通りCoinを検知して識別するものを試してみました。
準備するものは
- データ(Jpeg写真)100枚程度
- RecognizingObjectsInLiveCapture
- IBM Cloud Annotationsのアカウント(無料)
- 1円玉〜500円玉
以上です。

※Create MLはXcodeを右クリックでOpen Developer Toolにあるのでそこからmodelを作って下さい。

機械学習の大まかな種類

機械学習には
・Image Classification(画像の分類)
・Sound Classification(音の分類)
・Action Classification(動作の分類)
・Object Detection(物体の検知)
などがありますが、今回はObject Detectionの実装、つまり物体の検知をしていきます。

Objet Detectionとは

Object Detectionとは物体を検知をする事を指しますので”画像のどこになにが写っているか”と言うことを判定することになります。よって”写真”と”位置の情報”が必要となります。
写真はあらかじめ撮影し、フォルダ別に格納します。(フォルダ名=分類名)今回は硬貨を検知し、識別していきたいので500円〜1円のフォルダと、それぞれの画像の位置情報が必要となります。
→Object Detectionについてより深く知りたい方はこちらをご覧ください。
Create MLでのオブジェクト検出モデルのトレーニング

手順

  • データを集める。(検知したい写真をとる)
  • IBM Cloud Annotations(以下IBMCAとする)を使って集めたデータを分類分け、位置情報を指定してデータセットを作る
  • Create MLにIBMCAで作成したファイルを入れてModelを生成する
  • Create ML上でTestをしてみる
  • Appleのサンプルに入れて試してみる

写真をとる(データを用意する)

写真は今回Coinが写った写真を100枚ほど用意しました。
スクリーンショット 2020-09-26 19.10.21.jpg

写真は
・様々な角度の写真
・大きさも大・中・小の3パターンとしてそれぞれ30枚前後用意
・背景は統一して白とした

データセットを作る(JSONファイル)

Object Detectionでは写真のみではなくJSONファイルも必要となる(位置情報が必要な為)ので今回はIBM Cloud Annotationsのアカウント(無料)を使って作成した。作り方は以下の通りである。


・IBM Cloudを続行するをクリック


・新しいプロジェクトをクリック


・ローカリゼーションをクリック


・ファイル名を記載し、プロジェクトが立ち上がると写真をD&Dできるようになるので入れる


・Object(今回は硬貨)事にドラッグして囲んでいく
・右側にLabel名を記載できるので記載する


・一度記載したLabel名は左上のダウンリストより選択できるのでCoin事にLabel付すると効率がいい


・全ての写真にLabel付し終えた後はFileよりCreate MLで使用できるようにexportする


・このファイルが生成されれば成功


・フォルダの中にJSONファイルがあるか確認

Create MLでModel作成

データセットができたらXcodeを右クリックし、Create MLを立ち上げる

object Detectionを選択し、先ほど作成したデータセットのファイルをフォルダ事D&D

Iterationは学習トレーニングを繰り返す回数(だと思います)で多ければ良いってわけではないみたいですが極度に少なくても精度が出ないので甘い場合は少し上げてみて調節してみて下さい。
(今回300、600、1000と試したが変わらなかった。)

学習を終えたらTestし、問題なければModelを取り出します。(D&Dで取り出せる)

Recognizing Objects in Live Captureを使って試してみる

後はAppleが提供しているRecognizingObjectsInLiveCaptureをダウンロードして開く


ここにCreate MLで作ったModelをD&D

codeはもともとObjectDetectorと言うmodelを読み込んでクロワッサンやバナナ?などを検知して識別するものが入っているので
swift
guard let modelURL = Bundle.main.url(forResource: "ObjectDetector", withExtension: "mlmodelc")


swift
guard let modelURL = Bundle.main.url(forResource: "coindetection", withExtension: "mlmodelc")

今回生成したmodel名に書き換えて実装

大体成功かな?
大体2時間ぐらいでデータセットから実装までできるので是非お試しあれ!

最後に

今回はサンプルを使用しましたがせっかくなのでここもSwiftUIで今後作っていくことにチャレンジしていきたいと思います!またその他にも機械学習に関するデータセットの作り方から実装までチャレンジしていきたいと思いますので是非興味あるかたはこちらもチェックしてみて下さい!

Twitter
https://twitter.com/oka_yuuji
note
https://note.com/oka_yuuji

また筆者高校の教職員ですが、技術の習得、研究が大好きです!もし技術的なご依頼や研究に関する事であれば下記にてDMでご連絡下さい。
https://twitter.com/oka_yuuji

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

Install new IOS Version without upgrade xcode

https://github.com/filsv/iPhoneOSDeviceSupport
download the folder and copy to
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/

sudo rm -Rf ~/Library/*/*Xcode*
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【アプリ開発】AppStoreで動画を公開する方法

恥ずかしながら最近までAppStoreで動画を再生できることを知りませんでした…
そして今回は動画をアップロードする機会があったのでその方法を紹介したいと思います。
同じように動画のアップロードにお悩みの方のためになれば幸いです。

記事本文はこちらです。
https://tedenglish.site/upload-video-appstore/

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

【iOS14】PHPickerViewControllerからData型の画像データを取得するには?

PHPickerViewController

AppleのPHPickerViewControllerのサンプルコード を見ると、UIImageを取得する例は書かれているのですが、Dataを取得する例はありません。

当然ながら、UIImageをDataに変換するのは微妙です。

百歩譲って、HEICやJPGやPNGなら良いかもしれませんが、UIImageをDataに変換するメソッドは、「pngData()」と「jpegData(compressionQuality:)」の2つしかありません。
GIFをこれらのメソッドで変換してしまうとアニメーションが止まってしまいます。

やはり、直でData型を取得したい。
そんな人に向けて、 PHPickerViewControllerからDataを取得する方法 を書いておきます。

NSItemProviderのloadDataRepresentation(forTypeIdentifier:completionHandler:)を使えば画像のDataを取得できる

NSItemProviderの loadDataRepresentation(forTypeIdentifier:completionHandler:) を使えば取得できます。

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    results.forEach { result in
        result.itemProvider.loadDataRepresentation(forTypeIdentifier: "public.image", completionHandler: { [weak self] data, _ in
            guard let data = data else { return }
            print(data)
        })
    }
}

ミソとしては、forTypeIdentifier(Uniform Type Identifier)に public.image を指定することですね。
こうすることで、HEICやPNG、JPG、GIFなどのDataを取得できます。

もっと、取得するDataを細かく指定したい場合は、Appleの Uniform Type Identifier Conceptsを参考にして指定してください。

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

【iOS14】PHPickerViewControllerから直にData型の画像データを取得するには?

PHPickerViewController

AppleのPHPickerViewControllerのサンプルコード を見ると、UIImageを取得する例は書かれているのですが、Dataを取得する例はありません。

当然ながら、UIImageをDataに変換するのは微妙です。

百歩譲って、HEICやJPGやPNGなら良いかもしれませんが、UIImageをDataに変換するメソッドは「pngData()」と「jpegData(compressionQuality:)」の2つしかありません。
GIFをこれらのメソッドで変換してしまうとアニメーションが止まってしまいます。

やはり、直でData型を取得したい。
そんな人に向けて、 PHPickerViewControllerからDataを取得する方法 を書いておきます。

NSItemProviderのloadDataRepresentation(forTypeIdentifier:completionHandler:)を使えば画像のDataを取得できる

NSItemProviderの loadDataRepresentation(forTypeIdentifier:completionHandler:) を使えば取得できます。

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    results.forEach { result in
        result.itemProvider.loadDataRepresentation(forTypeIdentifier: "public.image", completionHandler: { [weak self] data, _ in
            guard let data = data else { return }
            print(data)
        })
    }
}

ミソとしては、forTypeIdentifier(Uniform Type Identifier)に public.image を指定することですね。
こうすることで、HEICやPNG、JPG、GIFなどのDataを取得できます。

もっと、取得するDataを細かく指定したい場合は、Appleの Uniform Type Identifier Conceptsを参考にして指定してください。

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

【Swift】タプルを用いたswitch文

1.はじめに


今回はSwitch文の条件分岐にタプルの値を使う、通常とは少し違った使い方を紹介しようと思います。場面によっては有効な使い方だと思うので、ぜひご覧ください。

2.タプルをcaseで使う


まず、タプルを定数のように考えてcaseラベルに置くようにすれば、次のように書くことができます。
ただし、タプルの型はすべて統一されている必要があり、この例では、変数dayが月と日からなる日付を表すタプルを持っていると考えます。
3つだけ書かれているいずれかの祝日に一致すれば表示されます。

switch day {
case (1,1): print("元旦")
case (2,11): print("建国記念の日")
case (5,3): print("憲法記念日")
default: break
}

ある範囲の日付を表すために、範囲演算子が使えます。例えば次の例を確認してください。

switch day {
case (1,1...5): // 1/1 ~ 1/5
    print("正月休み")

case (5,3): print("憲法記念日")

case (4,29), (5,2...6):
    print("連休")

default: break
}

値を列挙するだけでなく、タプルの要素を取り出して処理に使うこともできます。
このために、caseラベル内にlet、またはvarを記述して、case節の内部でだけ有効な定数、変数を宣言できます。

switch day {
case (5,3): print("憲法記念日")
case (8, let d): print("8/\(d)は夏休みです")
default: break
}

この例では、8月の日付ならいつでも夏休みになります。変数dを使う必要がなければ、「_」を使い次のように記述することもできます。

case (8, _): print("夏休み")

なお、キーワード付きのタプルをswitch文で使用することもできますが、値とcaseラベルで使っているキーワードが一致しない場合にはエラーになります。
タプルとcaseのラベルの対応付けのように、構造と値の一致を調べる処理一般を、Swiftではパターンマッチングといいます。

3.switch文でwhere節を使う


switch文で、タプルの要素に対して条件を付けることもできます。ここで、日付から曜日を計算する関数dayOfWeekを利用します。この関数は日曜なら0、月曜なら1、というように曜日を整数値で返します。なお、変数yearが西暦年を持つとします。
caseラベルに条件を付ける場合、ラベルと「:」の間に、予約語whereと条件を記述します。以下の例を確認してください。

switch day {
case (1, let d) where d >= 8 && d <= 14 && dayOfWeek(year,1,d) == 1:
    print("成人の日")

case (8, _): print("夏休み")

case (let m, let d) where dayOfWeek(year,m,d) == 0:
    print("\(m)/\(d)は日曜日")

default: break
}

ここでは、1月の第2月曜日が成人の日であることを条件にしています。
同様に、1年のいつでも日曜日であれば表示するようにしています。
このcaseラベルのように、タプルのすべての要素を定数に割り当てる場合、要素ごとにletを指定することもできますが、タプルの前に1つだけletを記述することもできます。

case let (m, d) where dayOfWeek(year,m,d) == 0:
    print("\(m)/\(d)は日曜日")

なお、caseラベルでletやvar、whereを使って条件を指定する方法は、タプルだけでなく、通常の整数などに対しても利用できます。

定数や変数を使うラベルは、別並べると「,」で区切って並べる際に制約があります。
これは、どのラベルの条件に一致したかによって、定数や変数が値を持たない可能性があるからです。
例えば以下のようにすることはできません。

switch t {
case (1, let y), (2, 2):
    print("\(y)")

case (2, _):
    fallthrough

case (let z, 2):
    print("\(z)")
default: break
}

caseラベルで定数や変数を使っているcase節へは、すぐ上の選択肢からfallthrough文で遷移することはできません。

ラベルを「,」で区切って並べた時、それらが定数や変数を含んでよい条件は、すべてのラベルが同じ名前、同じ型の定数もしくは変数を含むことです。
つまり、どんな場合でも定数もしくは変数の値が定まる必要があります。その点だけ気を付けてください。

4.オプショナル型をswitch文で使う


switch文で場合分けしたい式がオプショナル型の値を含む場合も、caseのラベルで書き分けることができます。
例として名前と年齢からなるタプル型を考えます。ただし、年齢は不明の場合があるのでオプショナル型になっています。

typealias People = (String, Int?)

このような型をSwitch文で扱うには、caseパターンで「?」という記号を使います。

switch m {
case let (name, age?) where age >= 18:
    print("\(name), 免許取得可")

case let (name, (15...18)?):
    print("\(name), 高校生")

case (let name, nil):
    print("\(name), 年齢不明")

default: break
}

このように、対応する定数もしくは変数に「?」を付けると、オプショナル型がnil以外の値を持つときにマッチし、その後のwhere節や実行分では開示演算子を使うことなく値を参照できます。
また、nilを記述しておけば、オプショナル型がnilの値を持つ場合にマッチします。範囲を示すパターンは()で囲みます。

オプショナルであることを示す「?」は、タプルなどの他のパターンに利用することができます。
例えばPepole?型の変数opsをswitch文で使うとすれば、caseには次のように記述できます。

switch ops {            // opsはPeople?型
case let (name, 18?)? : // 開示した結果が(文字列, 18)ならマッチする

5.おわりに


今回はタプルを用いたswitch文の利用法について書きました。次回は列挙型についての記事を書こうと思っているので、よろしければぜひご覧ください。
ここまで見てくださった方、ありがとうございました。

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

ReactNativeアプリの標準言語を日本語に変更する

はじめに

ReactNativeにあるShareライブラリやTextInputライブラリを使ってて、
動作チェックしてるときにNaitve部分の箇所が日本語じゃなかったので修正をする。

修正前

Text
スクリーンショット 2020-09-28 12.27.35.png

Share
スクリーンショット 2020-09-28 12.28.08.png

修正方法

Xcode起動PROJECTクリックInfoクリックLocalization native development region en からjpへ変更

修正後

Text
スクリーンショット 2020-09-28 12.25.03.png

Share
スクリーンショット 2020-09-28 12.29.36.png

まとめ

iOSの言語変更はこの方法でOKです。

AndroidはOSの設定で決まるようなので、必要はなさそうです。

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

iOS Keyboard ExtensionでadjustTextPositionするときのお作法

iOS Keyboard Extensionでカーソルを移動させるにはadjustTextPosition(byCharacterOffset:Int)を使います。ドキュメントにはバッチリ

To move the insertion point in the text input view, use the adjustTextPosition(byCharacterOffset:) method. For example, if you want to implement a forward delete action, move the insertion position forward by one character then delete backwards:

// Move the text insertion position forward 1 character (一文字分前に進める)
textDocumentProxy.adjustTextPosition(byCharacterOffset: 1)

と書いてありますが、罠です。なかなか気付きにくいところだと思うので共有しておきます。

この通りに実装するとこんな挙動になります。
ezgif-5-786edace6467.gif

「あいうえお」まではうまく動いていますが、漢字や絵文字に至ったあたりからどうも様子がおかしいですね。

解決方法

どうやらこのメソッドはutf16とした時の文字数で「一文字分」を数えています。ということで、実行前に進行方向の文字をチェックして、それからadjustTextPositionしましょう。

func getActualOffset(count: Int)->Int{
    if count>0{
        if let after = textDocumentProxy.documentContextAfterInput{
            if after == ""{
                return 1   //一文字前に改行がある場合、after==""となる。
            }
            let left = after.prefix(count)
            return left.utf16.count
        }else{
            return 1
        }
    }else if count<0{
        if let before = textDocumentProxy.documentContextBeforeInput{
            let right = before.suffix(-count)
            return -right.utf16.count
        }else{
            return -1
        }
    }else{
        return 0
    }
}

let offset = getActualOffset(count: 1) //正確なoffsetを取得する
textDocumentProxy.adjustTextPosition(byCharacterOffset: offset) //実行する

これで意図した通り動作します。
ezgif-5-410ac0a2f5c8.gif

気付きづらいところに潜んでいる落とし穴なので、くれぐれもお気をつけください。

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

Xcode12.0.1で 任意のOSバージョンのシュミレータを生成できない問題について

iOS14シュミレータで開発中のアプリの動作チェックをやりつつ、現行のiOS13で開発もしたい!!
最新版のXcode12を入れた!!!!

Xcode12ではiPhoneXや8のOSは14なんですね。こちらは問題なく動作できたので、ヨシッ!!

次にiOS13のシュミレータを作る!

シュミレータ生成方法
  シュミレータの場所を押下 → Add Additional Simulators押下

はいはいはいはい シュミレータ名付けて、OSVersionを13系統に変えて終わり終わr..
スクリーンショット 2020-09-28 10.22.40.png

おん?
スクリーンショット 2020-09-28 10.22.26.png

OSのバージョンが選べない。。。。というか存在してない。。。。
なんでやねん。ってことで色々調べました。

Xcode12.0でOSを指定してシュミレータを生成している人はいたのできっと自分の設定ミスなんだろうな。。

ん?Xcode12.0.1????
おま....いつの間に新しいのが出ていたんや・・・・・・
スクリーンショット 2020-09-28 10.23.01.png

シュミレータのOSを選べない理由は最新のXcodeのバージョンのせいかも。よし、バージョン下げよう。
AppStoreからでは最新のXcodeしか落とせないのでDevelopperから自分が使いたいXcodeのバージョンをダウンロードする必要が有ります。

https://developer.apple.com/download/more/
今回はXcode12.0が欲しいのでXcode12.xipを落とします。

スクリーンショット 2020-09-28 11.07.51_censored.jpg

落としたらインストールして、またシュミレータのOSを選定できる場所まで行くと....
スクリーンショット 2020-09-28 11.10.56.png

きました!!!これでiOS14とiOS13どちらの環境でも開発ができます!!!!

まとめ

シュミレータOSを選定できない問題はまだ解決されていないみたいなので現状はXcode12.0で開発するのが良さそうです。

Xcode関係DL
 https://developer.apple.com/download/more/
Appleの公式にも問題が上がっているようですので近いうちに修正されるのかなって思います。
 https://developer.apple.com/forums/thread/662072

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

【iOS14】起床時間と睡眠時間を完全自動でツイートするショートカットを作ってみた

はじめに

どうも、自動化大好き人間です。

少し前ですがiOS14がリリースされましたね。
iOS14では、ホーム画面のウィジェットや、Appライブラリ、空間オーディオ、睡眠機能などなど、盛り沢山のアップデートで個人的には神アプデでした。 また、Apple Watch Series6では血中酸素濃度が測定できるなど、ヘルスケア系にかなり力を入れているアップデートだと感じました。

さてさて、自動化が大好きな私ですが、このiOS14のアップデートで最も注目した機能の一つが、睡眠のオートメーションです。起床のタイミングでオートメーションを走らせられると色々できるんじゃないかなぁと思います。
そこで、今回は新しく追加された睡眠のオートメーションを使って、「起床したタイミングで、起床時間と睡眠時間を自動的にツイートする」ことを実現してみました。
これを使えばみなさんも睡眠に対するモチベーションが爆上がりすること間違いなし(?)です1 2

行った手順はこんな感じです。

  1. ショートカット「睡眠時間を計算」を作成する(ヘルスケアから睡眠のログを取得、計算)
  2. ショートカット「起床時間と睡眠時間をツイート」を作成する(IFTTT連携、ショートカット作成)
  3. オートメーションに組み込む

忙しい方へ

ショートカット内で別のショートカットを参照しています。上から順にダウンロードしてください。(IFTTTで作成したアプレットのwebhook URLが必要です。)

? 完成品

実装

1. ショートカット「睡眠時間を計算」を作成する

ヘルスケアから睡眠のログを取得

実際に睡眠のログを見てみると、ログが複数あることが分かるかと思います。これを知らずに実装してしまうと、最新の睡眠ログのみを取得して誤った睡眠時間を計算することになってしまいます。
そこで、睡眠ログを「ログの日付が過去1日以内」かつ「ログの終了時間が今日」という条件でログの抽出を行います。こうすることで対象の睡眠ログをすべて取得できるかと思います。

iOSショートカット ヘルスケア 睡眠ログ

睡眠のログから睡眠時間を計算する

次に、睡眠時間の計算を行います。
ショートカットアプリでは、for文のような繰り返しのアクションが用意されています。繰り返しの中で、睡眠ログの終了時間と開始時間を取得し、差分を取ることで睡眠ログから睡眠時間を計算することができます。

iOSショートカット 繰り返し

ちなみに、ショートカットアプリの計算アクションでは日付の演算ができないので、終了時間および開始時間をタイムスタンプに変換して計算を行っています。なお、日付からタイムスタンプに変換する処理は関数のように別のショートカットとして切り出して、そのショートカットを呼び出す形で変換を行いました。

繰り返しの中であらかじめ用意した変数に睡眠時間を足していくことで、合計の睡眠時間を計算することができます。あとは、睡眠時間を時間に変換するなどすれば準備完了です。

? 完成品

注意: ショートカット「睡眠時間を計算」は、ショートカット「日付をタイムスタンプに変換」を参照しています。「睡眠時間を計算」追加時にショートカット「日付をタイムスタンプに変換」を選択する必要があります。

2. ショートカット「起床時間と睡眠時間をツイート」を作成する

睡眠時間をツイートするアプレットを作成する

計算された睡眠時間を変数として、最後に結果をツイートします。
ショートカットアプリ内には公式Twitterのツイートするアクションが用意されていますが、この場合、手動でツイートボタンを押してツイートする必要があります。
そこで、IFTTTと連携してIncoming Webhookを叩くことで完全な自動化を目指します。(IFTTTとTwitterアカウントの連携は省略します。)
最近IFTTTが有料プランを発表されましたが、アプレット3つ以内であれば無料で使うことができます。

IF 「Webhooks (Receive a web request)」, Then 「Twitter(Post a tweet)」の形でアプレットを作成します。このとき、Webhooksを追加するときに設定するEvent Nameはメモしておきましょう。

webhooksを選択(このときのEvent Nameをメモっておく)
IFTTT webhooks

Twitterのアクションを設定
IFTTT twitter

ワークフロー全体図。[Continue] > [Finish]でアプレットを作成します。
IFTTT ワークフロー全体図

アプレットを作成したら、https://ifttt.com/maker_webhooks/settings から、webhookのURLを取得し、そのURLにアクセスします。Editを押すとURL(Key)がリフレッシュされるようなので注意が必要です。(紛らわしすぎ...)
IFTTT_webhooks_settings.png

メモっておいたURLにアクセスすると、Webhookのテスト画面が表示されます。
IFTTT_webhooks_settings_2.png

POSTでvalue1にツイートするテキストを設定して、以下のURLを叩けば任意のテキストをツイートすることができます。

curl -X POST https://maker.ifttt.com/trigger/{Event Name}/with/key/{Your KEY}

ショートカット「ショートカットの入力をツイート」を作成する

IFTTT上でアプレットが作成できたので、ショートカットアプリで任意のテキストをツイートするショートカットを作成します。
「URLの内容を取得」アクションでwebhookを叩くことができます。IFTTTのIncoming webhookは、POSTのパラメータにvalue1value2value3が設定可能です。アプレット作成時にvalue1をツイートするように設定したので、パラメータvalue1ショートカットの入力を設定しています。

iOSショートカット URLの内容を取得

? 完成品

注意: ショートカット追加時に、IFTTTのwebhook URLが尋ねられるので、あらかじめwebhook URLを用意しておきましょう。

ショートカット「起床時間と睡眠時間をツイート」を作成する

上記の「ショートカットの入力をツイート」するショートカットににテキストを渡す形でショートカットを作成します。

? 完成品

注意: ショートカット「起床時間と睡眠時間をツイート」をは、ショートカット「ショートカットの入力をツイート」を参照しています。「起床時間と睡眠時間をツイート」追加時にショートカット「ショートカットの入力をツイート」を選択する必要があります。

3. オートメーションに組み込む

今までに作成したショートカットをオートメーションに組み込みます。「睡眠」のオートメーションは、iOS14から新しく追加された機能です。

[新規オートメーション] > [個人用オートメーションを作成] > [睡眠] > [起床]にチェック > [次へ]
と選択していき、アクション設定画面からアクションを追加していきます。
「睡眠時間を計算」 > [起床時間と睡眠時間をツイート]とアクションを設定すれば作業は完了です。

アクションの設定

オートメーションの設定

おわりに

ショートカットをたくさんつくりました。
処理を切り分けて作ったので、ツイートではなくLINE Notifyで通知がしたいなどの応用が効きやすいかと思います。
ショートカットを作る過程の記事は調べてもあまり出こなかったので、この記事が誰かの助けになれば幸いです。

それでは、おやすみなさい〜?
(動かなかったらごめんなさい)


  1. パブリックコミットメントのテクニックとして、SNSを使って周囲の人に宣言することは目標達成率を上げるために効果的であると言われています。 

  2. 最近のAppleのCMのように個人情報 (睡眠時間) が筒抜けになります(笑) 

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

【入門】iOS アプリ開発 #10【ゲームの各種設定(難易度やスピードレベル)】

はじめに

今回はラウンドによって変化する難易度やスピードレベルの詳細を作り込み、#5【シーケンスの設計】で作成した各画面モードを結合して、ほぼ完成の状態に持っていく(以下がイメージ動画)。ソースコードは GitHub に公開しているので参照してほしい。

※YouTube動画
IMAGE ALT TEXT HERE

仕様書

ラウンドによって変化する難易度やスピードレベルの仕様は以下の通り。

Spec1.png

Spec2.png

Image3.png

今回、難易度表の外国用バージョンと、スパート②設定は作成しない。

難易度表とスピード表の実装

タプルの配列として、難易度表とスピード表を実装する。
以下、各画面モードの結合を除き、全て CgContextクラスに含める。

    enum EnLevel: Int {
        case Level_A = 0, Level_B, Level_C, Level_D
    }

    let table_difficultySettings: [(round: Int, levelOfSpeed: EnLevel, timeWithPower: Int, numberOfFeedsRemaingToSpurt: Int, levelOfAppearance: EnLevel, kindOfSpecialTarget: CgSpecialTarget.EnSpecialTarget, timeNotToEat: Int, intermission: Int)] = [
            //round, speedLevel, PowerTime[ms], Spurtfeeds, GhostAppear, SpecialTarget, NoEatTime[ms], Intermission
            (     1,   .Level_A,          6000,         20,    .Level_A,      .Cherry,           4000,            0 ),
            (     2,   .Level_B,          5000,         30,    .Level_B,  .Strawberry,           4000,            1 ),
            (     3,   .Level_B,          4000,         40,    .Level_C,      .Orange,           3000,            0 ),
            (     4,   .Level_B,          3000,         40,    .Level_C,      .Orange,           3000,            0 ),
            (     5,   .Level_C,          2000,         40,    .Level_C,       .Apple,           3000,            2 ),
            (     6,   .Level_C,          5000,         50,    .Level_C,       .Apple,           3000,            0 ),
            (     7,   .Level_C,          2000,         50,    .Level_C,       .Melon,           3000,            0 ),
            (     8,   .Level_C,          2000,         50,    .Level_C,       .Melon,           3000,            0 ),
            (     9,   .Level_C,          1000,         60,    .Level_C,    .Galaxian,           3000,            3 ),
            (    10,   .Level_C,          5000,         60,    .Level_C,    .Galaxian,           3000,            0 ),
            (    11,   .Level_C,          2000,         60,    .Level_C,        .Bell,           3000,            0 ),
            (    12,   .Level_C,          1000,         80,    .Level_C,        .Bell,           3000,            0 ),
            (    13,   .Level_C,          1000,         80,    .Level_C,         .Key,           3000,            3 ),
            (    14,   .Level_C,          3000,         80,    .Level_C,         .Key,           3000,            0 ),
            (    15,   .Level_C,          1000,        100,    .Level_C,         .Key,           3000,            0 ),
            (    16,   .Level_C,          1000,        100,    .Level_C,         .Key,           3000,            0 ),
            (    17,   .Level_C,             0,        100,    .Level_C,         .Key,           3000,            3 ),
            (    18,   .Level_C,          1000,        100,    .Level_C,         .Key,           3000,            0 ),
            (    19,   .Level_C,             0,        100,    .Level_C,         .Key,           3000,            0 ),
            (    20,   .Level_C,             0,        100,    .Level_C,         .Key,           3000,            0 ),
            (    21,   .Level_C,             0,        100,    .Level_C,         .Key,           3000,            0 ),
            (    22,   .Level_D,             0,        100,    .Level_C,         .Key,           3000,            0 )
        ]

    let table_speedSettings: [ (eatNone: Int, eatFeed: Int, eatPow: Int, eatNoneInPow: Int, eatFeedInPow: Int, eatPowInPow: Int,
         ghost: Int, ghostInSpurt: Int, ghostInPow: Int, ghostInWarp: Int) ]
        = [
            // Level A
            ( eatNone: 16, eatFeed: 15, eatPow: 13, eatNoneInPow: 18, eatFeedInPow: 17, eatPowInPow: 15,
              ghost: 15, ghostInSpurt: 16, ghostInPow: 10, ghostInWarp: 8 ),
            // Level B
            ( eatNone: 18, eatFeed: 17, eatPow: 15, eatNoneInPow: 19, eatFeedInPow: 18, eatPowInPow: 16,
              ghost: 17, ghostInSpurt: 18, ghostInPow: 11, ghostInWarp: 9 ),
            // Level C
            ( eatNone: 20, eatFeed: 19, eatPow: 17, eatNoneInPow: 20, eatFeedInPow: 19, eatPowInPow: 17,
              ghost: 19, ghostInSpurt: 20, ghostInPow: 12, ghostInWarp: 10 ),
            // Level D
            ( eatNone: 18, eatFeed: 17, eatPow: 15, eatNoneInPow: 18, eatFeedInPow: 17, eatPowInPow: 15,
              ghost: 19, ghostInSpurt: 20, ghostInPow: 10, ghostInWarp: 9 )
        ]

このタプルの配列からラウンドに合わせたデータを取り出して、CgContext クラスのメンバにそれぞれ設定する。

    /// Set difficulty of the round
    func setDifficulty() {
        let index = demo ? 0 : round-1
        let count = table_difficultySettings.count
        let table = (index < count) ? table_difficultySettings[index] : table_difficultySettings[count-1]

        levelOfSpeed = table.levelOfSpeed
        timeWithPower = table.timeWithPower
        numberOfFeedsRemaingToSpurt = table.numberOfFeedsRemaingToSpurt
        levelOfAppearance = table.levelOfAppearance
        kindOfSpecialTarget = table.kindOfSpecialTarget
        timeNotToEat = table.timeNotToEat
        intermission = table.intermission
    }

プレイヤー(パックマン)のスピードを取得するメソッドについては以下の通り。パワーエサを食べて逆転している時と、そうでない時で取得する値を変える。

    func getPlayerSpeed(action: CgPlayer.EnPlayerAction, with power: Bool ) -> Int {
        let index = levelOfSpeed.rawValue
        let count = table_speedSettings.count
        let table = (index < count) ? table_speedSettings[index] : table_speedSettings[count-1]

        switch action {
            case .Walking where !power : return table.eatNone
            case .Walking where  power : return table.eatNoneInPow
            case .EatingFeed where !power : return table.eatFeed
            case .EatingFeed where  power : return table.eatFeedInPow
            case .EatingPower where !power : return table.eatPow
            case .EatingPower where  power : return table.eatPowInPow
            case .EatingFruit where !power : return table.eatNone
            case .EatingFruit where  power : return table.eatNoneInPow
            default: return 16
        }
    }

モンスター(ゴースト)出現タイミングの実装

パックマンがプレイ開始からエサを食べた数 numberOfFeedsEated によって、レベル毎に出現するゴーストの数を返す。

    func getNumberOfGhostsForAppearace() -> Int {
        let numberOfGhosts: Int
        // Miss Bypass Sequence
        if playerMiss {
            if numberOfFeedsEatedByMiss < 7 {
                numberOfGhosts = 1
            } else if numberOfFeedsEatedByMiss < 17 {
                numberOfGhosts = 2
            } else if numberOfFeedsEatedByMiss < 32 {
                numberOfGhosts = 3
            } else {
                playerMiss = false
                numberOfGhosts = getNumberOfGhostsForAppearace()
            }
        } else {
            switch levelOfAppearance {
                case .Level_A:
                    if numberOfFeedsEated < 30 {
                        numberOfGhosts = 2
                    } else if numberOfFeedsEated < 90 {
                        numberOfGhosts = 3
                    } else {
                        numberOfGhosts = 4
                    }
                case .Level_B:
                    if numberOfFeedsEated < 50 {
                        numberOfGhosts = 3
                    } else {
                        numberOfGhosts = 4
                    }
                case .Level_C: fallthrough
                default:
                    numberOfGhosts = 4
            }
        }
        return numberOfGhosts
    }

波状攻撃の実装

スタート時からカウントしている時間によって、ChaseMode と ScatterMode を切り替える。レベル毎に ChaseMode の時間を判定する。

    func judgeGhostsWavyChase(time: Int) -> Bool {
        var chaseMode: Bool = false
        switch levelOfSpeed {
            case .Level_A:
                chaseMode = (time >= 7000 && time < 27000) || (time >= 34000 && time < 54000)
                         || (time >= 59000 && time < 79000) || (time >= 84000)
            case .Level_B:
                chaseMode = (time >= 7000 && time < 27000) || (time >= 34000 && time < 54000)
                         || (time >= 59000)
            case .Level_C: fallthrough
            case .Level_D:
                chaseMode = (time >= 5000 && time < 25000) || (time >= 30000 && time < 50000)
                         || (time >= 55000)
        }
        return chaseMode
    }

各画面モードの結合

最後に CgGameMainクラスに、今まで作成した各モードを結合していく。

  • アトラクトモード:CgSceneAttractMode
  • クレジットモード:CgSceneCreditMode ※今回作成 GameSequences.swift に追加
  • スタートモード:CgSceneMaze
  • プレイモード:CgSceneMaze実行中
class CgGameMain : CgSceneFrame {

    enum EnMainMode: Int {
        case AttractMode = 0, CreditMode, WaitForStartButton, StartMode, PlayMode
    }

    enum EnSubMode: Int {
        case Character = 0, StartDemo, PlayDemo
    }

    private var scene_attractMode: CgSceneAttractMode!
    private var scene_creditMode: CgSceneCreditMode!
    private var scene_maze: CgSceneMaze!
    private var subMode: EnSubMode = .Character

    init(skscene: SKScene) {
        super.init()

        // Create SpriteKit managers.
        self.sprite = CgSpriteManager(view: skscene, imageNamed: "pacman16_16.png", width: 16, height: 16, maxNumber: 64)
        self.background = CgCustomBackgroundManager(view: skscene, imageNamed: "pacman8_8.png", width: 8, height: 8, maxNumber: 2)
        self.sound = CgSoundManager(binding: self, view: skscene)
        self.context = CgContext()

        scene_attractMode = CgSceneAttractMode(object: self)
        scene_creditMode = CgSceneCreditMode(object: self)
        scene_maze = CgSceneMaze(object: self)
    }

    /// Event handler
    /// - Parameters:
    ///   - sender: Message sender
    ///   - id: Message ID
    ///   - values: Parameters of message
    override func handleEvent(sender: CbObject, message: EnMessage, parameter values: [Int]) {
        if message == .Touch {
            if let mode: EnMainMode = EnMainMode(rawValue: getSequence()) {
                if mode == .AttractMode || mode == .WaitForStartButton {
                    goToNextSequence()
                }
            }
        }
    }

    /// Handle sequence
    /// To override in a derived class.
    /// - Parameter sequence: Sequence number
    /// - Returns: If true, continue the sequence, if not, end the sequence.
    override func handleSequence(sequence: Int) -> Bool {
        guard let mode: EnMainMode = EnMainMode(rawValue: sequence) else { return false }

        switch mode {
            case .AttractMode: attarctMode()
            case .CreditMode: creditMode()
            case .WaitForStartButton: break // Forever loop
            case .StartMode: startMode()
            case .PlayMode: playMode()
        }

        // Continue running sequence.
        return true
    }

    // ============================================================
    //  Execute each mode.
    // ============================================================

    func attarctMode() {
        switch subMode {
            case .Character:
                scene_attractMode.resetSequence()
                scene_attractMode.startSequence()
                subMode = .StartDemo

            case .StartDemo:
                if !scene_attractMode.enabled {
                    context.demo = true
                    sound.enableOutput(false)
                    scene_maze.resetSequence()
                    scene_maze.startSequence()
                    subMode = .PlayDemo
                }

            case .PlayDemo:
                if !scene_maze.enabled {
                    subMode = .Character
                }
        }
    }

    func creditMode() {
        context.demo = false
        if scene_attractMode.enabled {
            scene_attractMode.stopSequence()
            scene_attractMode.clear()
        }
        if scene_maze.enabled {
            scene_maze.stopSequence()
            scene_maze.clear()
        }

        context.credit += 1
        scene_creditMode.resetSequence()
        scene_creditMode.startSequence()
        sound.enableOutput(true)
        sound.playSE(.Credit)
        goToNextSequence()
    }

    func startMode() {
        context.credit -= 1
        scene_creditMode.stopSequence()
        scene_maze.resetSequence()
        scene_maze.startSequence()
        goToNextSequence()
    }

    func playMode() {
        if !scene_maze.enabled {
            subMode = .Character
            goToNextSequence(EnMainMode.AttractMode.rawValue)
        }
    }

}

またアトラクトモードにはキャラクター紹介に加えてプレイのデモがある。
こちらは、demo のフラグで、スワイプ操作と予め用意した操作テーブルを切り替えることで簡単に実装できた。

操作テーブルと取得メソッドは CgContextクラスに実装。
スタートからのフレーム数によって方向を取り出す。

    let table_operationInDemo: [ (frameCount: Int, direction: EnDirection) ] = [
        (9, .Left), (36, .Down), (61, .Right), (82, .Down), (109, .Right), (133, .Up), (162, .Right),
        (189, .Up), (215, .Right), (238, .Down), (261, .Right), (308, .Down), (335, .Left), (523, .Up),
        (555, .Right), (569, .Up), (609, .Left), (632, .Up), (648, .Right), (684, .Up), (732, .Left),
        (831, .Down), (864, .Left), (931, .Up), (948, .Left), (970, .Up), (1063, .Right), (1113, .Down),
        (1157, .Right), (1218, .Down)
    ]

    func getOperationForDemo() -> EnDirection {
        guard(demoSequence < table_operationInDemo.count) else { return .None }
        let table = table_operationInDemo[demoSequence]
        var direction: EnDirection = .None
        if counterByFrame >= table.frameCount {
            direction = table.direction
            demoSequence += 1
        }
        return direction
    }

getOperationForDemo メソッドで操作方向を取得してプレイヤーに設定する。CgSceneMazeクラスの sequenceUpdating シーケンスに追加実装。

    func sequenceUpdating() {
        // Operate player in demonstration automatically.
        if context.demo {
            let direction = context.getOperationForDemo()
            if direction != .None {
                player.targetDirecition = direction
            }
        }

        // 以下、省略

まとめ

ようやく、ほぼ完成の状態になった。

ゲームの動作スピードも問題ない。
ソースコードも 5000行程度のままで、結構簡単にできるものだ。

次回は、せっかく自作しているので色々とアレンジを行って完成としたい。

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