【2018年版】iOSアプリ新規で作る時にやること①

これ年に10回くらいはやるんだけど、どうにかならんのかなぁ。シェルスクリプト書けばいけそうだけど、Xcodeについてくのだるいし、労力と比較して迷う。

Carthage / RxSwift / Compass / XCGLogger / Reachability / Alamofire / R.swift / SwiftLint / Generamba / VIPER / fastlaneあたりが技術キーワードでしょうか。箇条書きでいきまっせ。
ぼくはもうCarthage対応してないやつは使わないので、Cocoapodsは出てきません。

①プロジェクト作成~設定・各種ライブラリ導入編です。



Apple Developerアカウントもらう

誰かにもらいましょう

App ID決める

誰かに決めてもらいましょう

PROJECT作る

作りましょう。

.gitignore投入

### https://raw.github.com/github/gitignore/160d27e2bebf784c4f4a1e070df057f3868b62bc/Objective-C.gitignore

# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated
build/
DerivedData/

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/

## Other
*.moved-aside
*.xcuserstate

## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
Carthage/Checkouts
Carthage/Build

# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/

*.generated.swift

vendor/bundler



PROJECT→Info

iOS Deployment Target

誰かに決めてもらいましょう

Configurations

Debug→ProductionDebug
Release→ProductionRelease
に変更して
それぞれをDuplicateして
StagingDebugとStagingReleaseを作る。
Developが必要ならDevelopDebugとDevelopReleaseも。
でもDevelopはあんま作りたくない。なんでiOSエンジニアがローカルにバックエンドの開発環境作らなきゃいけないのかうんぬんかんぬん。
Productionも正直用意するか迷う。何人かで開発するときはいらないかも。

Schema

デフォルトでできてるやつにsuffix:_Productionをつける
Duplicateして_Stagingを作る
両方Sharedにする
_Stagingのほうを編集。Build ConfigurationをStagingDebugとStagingReleaseに変更。

PROJECT→Build Settings

Versioning

Current Project Version:1
Versioning System:Apple Generic
に変更。

Swift Compiler - Custom Flags→Other Swift Flags

ProductionDebug:-D DEBUG
StagingDebug: -D DEBUG -D STAGING
追加。

TARGETS→General

Identity→Version
Signing
Deployment Info
をいじる
Deployment Info→Main Interfaceはクリアして、Main.storyboardを削除。

TARGETS→Capabilities

必要なのをオン

TARGETS→Info

Bundle Name:$(BundleNamePrefix)アプリ名

TARGETS→Build Settings

User-DefinedにBundleNamePrefixとConfigurationNameを追加。Stagingだけ設定をお好みで追加。
Packaging→Product Bundle Identifier:[お好みで]$(ConfigurationName)

ディレクトリ整理

めんどいので省略。



SwiftLint導入

インストール。グローバルにいれるのすきじゃないんだけど、Build Phaseで使いたいのでまぁしゃーない。もう、homebrewのインストールはいいっしょ。。このブログ内検索すればあるはず。

brew install swiftlint

TARGETS→Build PhaseでNew Run Script Phase

#!/bin/sh

if which swiftlint >/dev/null; then
  swiftlint autocorrect
  swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

で、プロジェクトルートに.swiftlint.ymlを用意。

included:
  - [Project Module Nameを]

excluded:
  - Pods/
  - Carthage/
  - fastlane
  - [Project Module Nameを]/Application/R.generated.swift

disabled_rules:
  - force_cast
  - force_try
  - force_unwrapping

file_length:
  warning: 1000
  error: 1500
type_body_length:
  warning: 500
  error: 750
function_body_length:
  warning: 200
  error: 300
line_length:
  warning: 200
  error: 300
variable_name:
  min_length: 1

で、ビルドエラーになったら修正。



Carthage導入

インストール。

brew install carthage

プロジェクトルートにCartfileを作る。基本は以下。
バージョンは2018/06現在の最新。

# General
github "ReactiveX/RxSwift" "4.2.0"
github "hyperoslo/Compass" "6.0.0"

# Resources
github "mac-cain13/R.swift.Library" "v4.0.0"

# Debug
github "DaveWoodCom/XCGLogger" "6.0.4"

# API
github "ashleymills/Reachability.swift" "v4.1.0"
github "Alamofire/Alamofire" "4.7.2"

作れたら

carthage update --cache-builds --platform iOS

Cartfileを変更したら毎回これやりましょう。初回はものすごい時間がかかりまっせ。
TARGETS→General→Linked Frameworks and Librariesで+→Add Other...で
Carthage/Build/iOSの下にできた.frameworkたちを追加。

f:id:devdevdev:20180628075150p:plain

TARGETS→Build PhaseでNew Run Script Phase

/usr/local/bin/carthage copy-frameworks

Input Filesにさっき入れたFrameworkを全て追加。。。

f:id:devdevdev:20180628075608p:plain

・7/1追記
github.com
公式の説明の通り、ドラッグ&ドロップだとなんかうまくいかなかった。
たぶんOutput Filesもちゃんと書いたほうがよい気がする



R.swift導入

github.com
ここから任意のバージョンのzipをダウンロードして解凍。
rswiftをプロジェクト内のお好みの場所に。

TARGETS→Build PhaseでNew Run Script Phase
これはComple Sourcesより前にやったほうがよい(上から順番に実行されるから上のほうにおくとよい)と思う。

"$PROJECT_DIR/rswiftのパス" generate "$PROJECT_DIR/R.generated.swiftを置きたいディレクトリのパス(.swiftlint.yml参照)"

ビルドするとR.generated.swiftができるので、プロジェクトに追加しておきましょう。



【Android】SimpleRecyclerViewAdapter

めずらしくあんどろいだーーーーーー
ねいてぃぶはらくちんでよいね!!!最高!ねむい!体調悪い!つらい!でも納期も時間も待ってくれない!

はい、RecyclerViewAdapterね。簡単なやつならこうしましょう。



app/build.gradle

抜粋です。databindingも使うのでね。

apply plugin: 'kotlin-kapt'
android {
    dataBinding {
        enabled = true
    }
}
dependencies {
    ext.android_support_version = "27.1.1"
    implementation "com.android.support:recyclerview-v7:$android_support_version"
}



SimpleRecyclerViewAdapter.kt

おもむろに以下をじっそうしましょう。

package hoge;

import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import java.lang.reflect.ParameterizedType

abstract class SimpleRecyclerViewAdapter<V : View, I : Any?>(defaultItems: ArrayList<I>? = null) : RecyclerView.Adapter<SimpleRecyclerViewAdapter<V, I>.ItemViewHolder>() {

    protected val isEmpty get() = !items.any()

    private val items: ArrayList<I> = arrayListOf()

    init {
        defaultItems?.let { items.addAll(it) }
    }

    fun removeAll() {
        items.clear()
        notifyDataSetChanged()
    }

    fun addItem(item: I) {
        items.add(item)
        notifyItemInserted(items.count())
    }

    fun removeItem(item: I) {
        val index = items.indexOf(item)
        items.removeAt(index)
        notifyItemRemoved(index)
    }

    fun updateItem(position: Int, item: I) {
        items[position] = item
        notifyItemChanged(position)
    }

    final override fun getItemCount() = items.count()

    final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(onCreateItemView(parent.context).apply {
        layoutParams = RecyclerView.LayoutParams(
                RecyclerView.LayoutParams.MATCH_PARENT,
                RecyclerView.LayoutParams.WRAP_CONTENT
        )
    })

    @Suppress("UNCHECKED_CAST")
    final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        (holder.itemView as? V)?.let {
            onBindItemView(it, items[position], position)
        }
    }

    @Suppress("UNCHECKED_CAST")
    protected open fun onCreateItemView(context: Context) =
            ((javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<*>)
                    .constructors.first().newInstance(context) as V

    protected abstract fun onBindItemView(itemView: V, item: I, position: Int)

    inner class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view)
}



使い方

view_hoge_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/textViewTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
HogeListItemView.kt
package hoge

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import hoge.R
import kotlinx.android.synthetic.main.view_hoge_list_item.view.*

internal class HogeListItemView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
    : LinearLayout(context, attrs, defStyleAttr) {

    init {
        LayoutInflater.from(context).inflate(R.layout.view_hoge_list_item, this)
    }

    fun setTitle(title: String) {
        textViewTitel.text = title
    }
}



HogeFragment.kt

省略。Bindingのサンプル書くのめんどい。

HogeFragmentVM.kt
package hoge

import android.content.Context
import hoge.SimpleRecyclerViewAdapter
import hoge.HogeListItemView

internal class HogeFragmentVM() {
    val recyclerViewAdapter = object : SimpleRecyclerViewAdapter<HogeListItemView, String>(arrayListOf<String>("Hogeeeeee", "Aaaaaaaaaaa")) {
        override fun onBindItemView(itemView: HogeListItemView, item: String, position: Int) {
            itemView.setTitle(item)
        }
    }
}
fragment_hoge.xml

抜粋。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="vm"
            type="hoge.HogeFragmentVM" />
    </data>
    <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="android.support.v7.widget.LinearLayoutManager"
        app:recyclerViewAdapter="@{vm.recyclerViewAdapter}" />
</layout>


onBindItemViewだけoverrideすればいいから簡単ですね。はい。いじょー

Prism PubSubEventを簡単に


GetEventとか長ったらしい。

おもむろに以下のクラスを実装しましょう。

using Prism.Events;
using System;

namespace Hoge
{
    public abstract class StaticPubSubEvent<T> : PubSubEvent where T : StaticPubSubEvent<T>, new()
    {
        private static EventAggregator eventAggregator = new EventAggregator();

        private static readonly T instance = eventAggregator.GetEvent<T>();

        protected StaticPubSubEvent() { }

        public static new void Publish() => eventAggregator.GetEvent<T>().InternalPublish();

        public static new SubscriptionToken Subscribe(Action action) => Subscribe(action, ThreadOption.PublisherThread);

        public static new SubscriptionToken Subscribe(Action action, ThreadOption threadOption) => Subscribe(action, threadOption, false);

        public static new SubscriptionToken Subscribe(Action action, bool keepSubscriberReferenceAlive) => Subscribe(action, ThreadOption.PublisherThread, keepSubscriberReferenceAlive);

        public static new SubscriptionToken Subscribe(Action action, ThreadOption threadOption, bool keepSubscriberReferenceAlive)
        {
            var actionReference = new DelegateReference(action, keepSubscriberReferenceAlive);

            EventSubscription subscription;
            switch (threadOption)
            {
                case ThreadOption.PublisherThread:
                    subscription = new EventSubscription(actionReference);
                    break;
                case ThreadOption.BackgroundThread:
                    subscription = new BackgroundEventSubscription(actionReference);
                    break;
                case ThreadOption.UIThread:
                    if (instance.SynchronizationContext == null) throw new InvalidOperationException();
                    subscription = new DispatcherEventSubscription(actionReference, instance.SynchronizationContext);
                    break;
                default:
                    subscription = new EventSubscription(actionReference);
                    break;
            }

            return eventAggregator.GetEvent<T>().InternalSubscribe(subscription);
        }
    }
}


で、イベントクラスを用意

namespace Hoge
{
    public sealed class HogeEvent : StaticPubSubEvent<HogeEvent> { }
}

Subscriber

HogeEvent.Subscribe(() =>
{
}, ThreadOption.UIThread);

Publisher

HogeEvent.Publish();

ぼくは気にしないけど、若干いまいちですね。何がいまいちかわかる人だけ自己責任で使ってくださいw(ヒント:EventAggregator


Enum Radio Button Binding


WPFのMVVMは好きです。他の言語で似非MVVM書いてるとなんだかなーって思う。
が、やっぱりWPFは嫌いです。

はい。EnumをBindingしたいとき。

まずおもむろにEnumの拡張を用意しましょう。このブログでも出てきたと思いますよー。

public static class EnumEx
{
    public static string StringValue(this Enum e)
    {
        var fieldInfo = e.GetType().GetField(e.ToString());
        var attribute = (StringValueAttribute)fieldInfo.GetCustomAttributes(typeof(StringValueAttribute), false).FirstOrDefault();
        return attribute == null ? null : attribute.StringValue;
    }
}


次にConverterを2つ用意しましょう。

using System;
using System.Globalization;
using System.Windows.Data;

namespace Hoge
{
    [ValueConversion(typeof(Enum), typeof(bool))]
    public class EnumToRadioButtonConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value.Equals(Enum.Parse(value.GetType(), parameter.ToString()));
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (!(bool)value)
                return Binding.DoNothing;
            return Enum.Parse(targetType, parameter.ToString());
        }
    }
}
using System;
using System.Globalization;
using System.Windows.Data;

namespace Hoge
{
    [ValueConversion(typeof(Enum), typeof(string))]
    public class EnumToStringConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return (Enum.Parse(value.GetType(), parameter.ToString()) as Enum).StringValue();
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (!(bool)value)
                return Binding.DoNothing;
            return Enum.Parse(targetType, parameter.ToString());
        }
    }
}


んで、enumとViewModel。双方向は省略。INotifyPropertyChangedを実装してくだされ。

namespace Hoge
{
    public enum Gender
    {
        [StringValue("男性")]
        Male,
        [StringValue("女性")]
        Female
    }

    public sealed class HogeViewModel
    {
        public Gender Gender { get; set; }
    }
}


で、View

<Page x:Class="Hoge.SamplePage"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:Hoge"
            mc:Ignorable="d" 
            d:DesignHeight="500" d:DesignWidth="300">
    <Page.DataContext>
        <local:HogeViewModel x:Name="viewModel" />
    </Page.DataContext>
    <Page.Resources>
        <local:EnumToRadioButtonConverter x:Key="EnumToRadioButton"/>
        <local:EnumToStringConverter x:Key="EnumToString"/>
    </Page.Resources>
    <Grid>
        <StackPanel Orientation="Horizontal">
            <RadioButton GroupName="Gender"
                 Content="{Binding Gender, ConverterParameter=Male, Converter={StaticResource EnumToString}}"
                 IsChecked="{Binding Gender, ConverterParameter=Male, Converter={StaticResource EnumToRadioButton}}"/>
            <RadioButton GroupName="Gender"
                 Content="{Binding Gender, ConverterParameter=Female, Converter={StaticResource EnumToString}}"
                 IsChecked="{Binding Gender, ConverterParameter=Female, Converter={StaticResource EnumToRadioButton}}"/>
        </StackPanel>
    </Grid>
</base:BasePage>

Windowsタスクトレイ常駐型のアプリを作る(WPF版)


よく作るんですけどね。忘れるよね。
なお、ググるキーワードはNotifyIcon。

準備

icoファイルを用意しましょう。
ここにあげようと思ったら、HatenaBlogさんはicoファイルに対応してないようなので皆さん頑張って自力で用意しましょう。

icoファイルをプロジェクトに投入

Resourcesフォルダを作って、ドラッグ&ドロップで投入しましょう。

f:id:devdevdev:20180502212145p:plain

Component作成

Componentsフォルダを作って、右クリック→「追加」→「新しい項目」
コンポーネントクラス」を探し出して、お好きな名前をつけて追加しましょう。
ここでは「NotifyIcon」としました。
f:id:devdevdev:20180502212706p:plain
f:id:devdevdev:20180502212745p:plain

NotifyIconクラスの編集

ツールボックスから「ContextMenuStrip」と「NotifyIcon」を追加します。
ツールボックスで検索して、ドラッグ&ドロップで持っていきましょう。
デフォルトの名前が嫌なので変えましたよ。

f:id:devdevdev:20180502213314p:plainf:id:devdevdev:20180502213319p:plain
f:id:devdevdev:20180502213354p:plain



contextMenuStripの設定

contextMenuStripを右クリックで「項目の編集」

f:id:devdevdev:20180502213605p:plain

MenuItemを追加
「デザイン」→「(Name)」と「表示」→「Text」を編集
Nameはご自由に。Textはタスクトレイのアイコンを右クリックしたときに出るメニューに表示されますよ。

f:id:devdevdev:20180502213811p:plain
f:id:devdevdev:20180502213933p:plain

myNotifyIconの編集

プロパティを開いて、ContextMenuStripとIconを設定

f:id:devdevdev:20180502214154p:plain



NotifyIcon.csの編集

終了メニュークリックしたらアプリケーションを終了。

using System;
using System.ComponentModel;
using System.Windows;

namespace Hoge.Components
{
    public partial class NotifyIcon : Component
    {
        public NotifyIcon()
        {
            InitializeComponent();
            toolStripMenuItemExitApp.Click += delegate (object sender, EventArgs e)
            {
                Application.Current.Shutdown();
            };
        }

        public NotifyIcon(IContainer container)
        {
            container.Add(this);

            InitializeComponent();
        }
    }
}



App.xamlの編集

App.xaml.csのほうから

using Hoge.Components;
using System.Windows;

namespace Hoge
{
    /// <summary>
    /// App.xaml の相互作用ロジック
    /// </summary>
    public partial class App : Application
    {
        private NotifyIcon notifyIcon = new NotifyIcon();

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            ShutdownMode = ShutdownMode.OnExplicitShutdown;
        }

        protected override void OnExit(ExitEventArgs e)
        {
            base.OnExit(e);
            notifyIcon.Dispose();
        }
    }
}

んで、App.xaml
StartupUri="MainWindow.xaml"って記載があるので削除しましょう。

<Application x:Class="Questionnaire.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:Hoge">
    <Application.Resources>
         
    </Application.Resources>
</Application>

参照追加

「System.Drawing」への参照を追加しましょう。疲れたので詳細は説明しません。Windowsエンジニアならわかるでしょ!

いじょ。


Windowsインストーラーの作成


2018年最新版!
Visual Studio2017向け!ぼくはVisual Studio Community 2017だよ!
既存のソリューションにインストーラープロジェクトを追加するよ!最近C#ばっかだよ!

インストール

marketplace.visualstudio.com

こちらからダウンロードしてインストール。

作成

Visual Studioを起動。インストーラーを追加したいソリューションを開いてね!

f:id:devdevdev:20180425100227p:plain
ソリューションを右クリックで「追加」→「新しいプロジェクト」

f:id:devdevdev:20180425100644p:plain
「インストール済み」→「その他のプロジェクトの種類」→「Setup Project」で名前をつけて「OK」



インストールするファイルの指定

f:id:devdevdev:20180425101138p:plain
追加されたプロジェクトを右クリックで「View」→「ファイルシステム
ちなみに「インストール」「アンインストール」からインストーラーとあんインストーラーが起動できるよ!

f:id:devdevdev:20180425101430p:plain
f:id:devdevdev:20180425101708p:plain
単純なものだったら「Application Folder」を右クリック→「Add」→「プロジェクト出力」でexeとかDLLが追加できるよ!
よその記事見るとインストーラービルド時に構成を切り替えてーとかやってたけど、インストーラーなんてここでRelease Any CPUにしとけばいいと思うよ!

インストーラー設定

必要最低限。セットアッププロジェクトのプロパティね。

f:id:devdevdev:20180425102053p:plain

Author→お名前を書きましょう
Manufacturer・ProductName→C:¥Program Files¥[Manufacturer]¥[ProductName]にデフォルトでインストールされるよ!
TargetPlatform→ちゃんと合わせようね!

インストーラー作成

f:id:devdevdev:20180425103046p:plain
ビルド・リビルドしましょう。
フォルダのどっかにexeとmsiができます。


Docker環境構築(Rails)②

devdevdev.hatenablog.com
基礎編はこちら

前回作ったコンテナ上にrailsの実行環境を作っていきますよー
ホスト側は一切汚しません。

とりあえずね
$ docker-compose run app bundle init

app/Gemfileが生成されるよ

Gemfileを編集

デフォルトはこう

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"

最後の行を以下のように書き換えます

gem 'rails', '5.1.4'



docker-compose.ymlの編集

appの下に以下を追記

    environment:
      RAILS_ENV: development

app / volumesの下に以下を追記

      - bundle:/usr/local/bundle

appの一番下に以下を追記

    command: /bin/sh -c 'mkdir -p tmp/sockets/ && bundle exec puma -C config/puma.rb'

一番下に以下を追記

volumes:
  bundle:



Dockerfileの編集

docker/app/Dockerfileに以下を追記

RUN apt-get update && apt-get install -y nodejs

ADD ./app/Gemfile* $APP_ROOT/
RUN bundle install

ビルド

$ docker-compose build



railsアプリケーション作成

appにsshではいる

$ docker-compose run app bash

普通に作る

$ rails new . -d mysql

Gemfileを上書きしていいか聞かれるので「Y」

app/config/database.yml書き換え

development:
  password: password

app/config/puma.rbに以下を追記

app_root = File.expand_path("../..", __FILE__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"

ビルドして起動

$ docker-compose build
$ docker-compose up

http://localhost/にアクセスしてrailsの画面が表示されればおk。