カテゴリー
C++ JUCE

JUCEでXmlファイルを扱う

juce::Fileクラスのインスタンスと、

const juce::File PresetManager::colorsFile { juce::File::getSpecialLocation(
  juce::File::SpecialLocationType::userApplicationDataDirectory)
  .getChildFile(ProjectInfo::companyName)
  .getChildFile(ProjectInfo::projectName)
  .getChildFile("UserSettings")
  .getChildFile("Colors.preset")
};

以下のような色の情報をhexで記録しているXmlファイルがあるとします。

<Colors>
  <Color quality="Major" hex="ffbe5050"/>
  <Color quality="Minor" hex="ffbe5050"/>
  <Color quality="minorMajor" hex="ffbe5050"/>
  <Color quality="Dominant" hex="ffbe5050"/>
</Colors>

このファイルをreadする場合は、以下のようにします。一番外側のタグ<Colors>は特に指定しなくても勝手にparseしてくれるようです。

std::unique_ptr<juce::XmlElement> colorsData;

void readXml() {
  colorsData = juce::XmlDocument::parse(colorsFile);
  for(auto* c: colorsData->getChildIterator()) {
    DBG(c->getStringAttribute("quality");
  }
}

// prints
// Major
// Minor
// minorMajor
// Dominant

二段構造のXmlの作成と読み込みは以下のコード例

if(!colorsFile.existsAsFile()) {
  // parent
  juce::XmlElement parentXml("DATA");
    
  // controls
  auto controlsXml = std::make_unique<juce::XmlElement ("Controls");
    
  auto childXmlSlider = std::make_unique<juce::XmlElement>("Color");
  childXmlSlider->setAttribute("control", SLIDER_COLOR);
  std::string trackColor = "fff28d11";
  childXmlSlider->setAttribute("hex", trackColor);

  // update with a method
  colorHandler.updateHex(SLIDER_COLOR, trackColor);
  controlsXml->addChildElement(childXmlSlider.release());
    
  // chords
  auto chordsXml = std::make_unique<juce::XmlElement>("Chords");
  auto childXmlMinor = std::make_unique<juce::XmlElement>("Color");
  childXmlMinor->setAttribute("quality", MINOR_NAME);
  std::string minorColor = "ff3488c3";
  childXmlMinor->setAttribute("hex", minorColor);
  colorHandler.updateHex(MINOR_NAME, minorColor);
  auto childXmlMinorMajor = std::make_unique<juce::XmlElement>("Color");
  childXmlMinorMajor->setAttribute("quality", MINOR_MAJOR_NAME);
  std::string minMajorColor = "ff4e7e4f";
  childXmlMinorMajor->setAttribute("hex", minMajorColor); // greenyellow
  colorHandler.updateHex(MINOR_MAJOR_NAME, minMajorColor);
  auto childXmlMajor = std::make_unique<juce::XmlElement>("Color");
  childXmlMajor->setAttribute("quality", MAJOR_NAME);
  std::string majorColor = "ffeeb42b";
  childXmlMajor->setAttribute("hex", majorColor); // orange
  colorHandler.updateHex(MAJOR_NAME, majorColor);
  auto childXmlDominant = std::make_unique<juce::XmlElement>("Color");
  std::string dominantColor = "ff7fa080";
  childXmlDominant->setAttribute("quality", DOMINANT_NAME);
  childXmlDominant->setAttribute("hex", dominantColor); // purple
  colorHandler.updateHex(DOMINANT_NAME, dominantColor);
  chordsXml->addChildElement(childXmlMinor.release());
  chordsXml->addChildElement(childXmlMinorMajor.release());
  chordsXml->addChildElement(childXmlMajor.release());
  chordsXml->addChildElement(childXmlDominant.release());
    
  parentXml.addChildElement(controlsXml.release());
  parentXml.addChildElement(chordsXml.release());
  if(!parentXml.writeTo(colorsFile)) {
    DBG("Failed to create Colors xml file");
  } 
} else {
  // if exists, read and update hex with it.
  // returns the outmost XmlElement*
  auto xml = juce::XmlDocument::parse(colorsFile);
  if(xml != nullptr) {
    for(auto child : xml->getChildIterator()) {
      if(child->hasTagName("Controls")) {
        for(auto control: child->getChildIterator()) {
          const auto name = control->getStringAttribute("control");
          const auto hex = control->getStringAttribute("hex");
          colorHandler.updateHex(name, hex);
        }
      }
      else if (child->hasTagName("Chords")) {
        for(auto chords: child->getChildIterator()) {
          const auto name = chords->getStringAttribute("quality");
          const auto hex = chords->getStringAttribute("hex");
          colorHandler.updateHex(name, hex);
        }
      }
    }
  }
}

voicingsFileというxmlファイル内に以下のデータがあるとします。

<DATA>
  <Voicings>
    <Voicing index="1" name="minor7 11 9top" quality="Minor">
      <Voice value="1"/>
      <Voice value="11"/>
      <Voice value="16"/>
      <Voice value="18"/>
      <Voice value="27"/>
    </Voicing>
    <Voicing index="2" name="minor7 11 b3top" quality="Minor">
      <Voice value="1"/>
      <Voice value="11"/>
      <Voice value="18"/>
      <Voice value="20"/>
      <Voice value="28"/>
    </Voicing>
    <Voicing index="3" name="minor7 11 11top" quality="Minor">
      <Voice value="1"/>
      <Voice value="11"/>
      <Voice value="16"/>
      <Voice value="27"/>
      <Voice value="30"/>
    </Voicing>
  </Voicings>
</DATA>
// auto xmlはTagName "DATA"のxmlElementのunique_ptrです。
auto xml = juce::XmlDocument::parse(voicingsFile);

// auto voicingsXmlはTagName "Voicings"のxmlElementのunique_ptrです。
auto voicingsXml = xml->getChildByName("Voicings");

// child elements からattributeで検索することができます。
auto voicingInInterest = voicingsXml->getChildByAttribute("index", std::to_string(index));

// 削除する場合はremoveChildElementを使います。
voicingsXml->removeChildElement(voicingInInterest, true);
カテゴリー
C++ JUCE

JUCEでRectangleをdrawする

JUCEで四角いシェイプを描画する方法として、カスタムComponentクラスを用意し、paintメソッドに記述します。

AudioProcessorEditorクラスにCustomComponentをレイアウトする場合、以下のpattern 1 〜 3は同じ結果をもたらします。

// CustomComponent.h

class CustomComponent: public juce::Component {
public:

  void paint(juce::Graphics& g) override
  {
    // pattern 1
    g.fillAll(juce::Colurs::orange);
    
    // pattern 2
    auto rect = getLocalBounds();
    g.setColour(juce::Colours::orange);
    g.fillRect(rect);

    // pattern 3
    g.setColour(juce::Colours::orange);
    juce::Path path;
    path.startNewSubPath(rect.getX(), rect.getY());
    path.lineTo(rect.getX(), rect.getHeight());
    path.lineTo(rect.getWidth(), rect.getHeight());
    path.lineTo(rect.getWidth(), rect.getY());
    path.closeSubPath();
    g.fillPath(path); 

    // pattern 4
    g.setColour(juce::Colours::orange);
    juce::Path path;
    // directly adding rect to the path
    auto rect = getLocalBounds();
    path.addRectangle(rect);
    g.fillPath(path);

    // pattern 5
    g.setColour(juce::Colours::orange);
    juce::Path path;
    auto rect = getLocalBounds();
    // draw rounded corner
    float cornerRadius = 10.f;
    path.addRoundedRectangle(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight(), cornerRadius);
    g.fillPath(path);

    // pattern 6
    // draw rounded rect using Rectangle<float>
    const float borderWidth = 2.f;
    const float cornerRadius = 10.f;
    const juce::Rectangle<float> rect = getLocalBounds().toFloat().reduced(borderWidth / 2.f);
    
    g.setColour(juce::Colours::grey);
    g.drawRoundedRectangle(rect, cornerRadius, borderWidth);

    // pattern 7
    auto rect = getLocalBounds();
    g.setColour(juce::Colours::black);
    juce::Path path;
    path.addRoundedRectangle(rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight(), rect.getWidth/2);
    g.fillPath(path);
  }

  
}
// AudioProcessorEditor.cpp

void AudioProcessorEditor::resized()
{
  auto area = getLocalBounds();
  auto width = area.getWidth();
  auto height = area.getHeight();
  float componentWidth = 100.f;
  float componentHeight = 100.f;
  customComponent.setBounds(width/2 - componentWidth/2, height/2 - componentHeight/2, componentWidth, componentHeight);
}
カテゴリー
C++

C++ Observer Pattern

C++でObserver, Subjectパターンを使ってCustom Listenerを作るサンプルコード

class CustomSubjectInterface {
public:
  class Listener {
  public:
    virtual ~Listener() {}
    virtual void onUpdated(CustomSubjectInterface* subject) = 0;
  };
  virtual ~CustomSubjectInterface() {}
  virtual void addListener(Listener* listener) = 0;
  virtual void removeListener(Listener* listener) = 0;
  virtual void notifyListeners(CustomSubjectInterface* subject) = 0;
};

class BoolSubject: public CustomSubjectInterface {
public:
  
  enum StatusCode {
    Success, Error
  };
  
  void addListener(CustomSubjectInterface::Listener* listener) override
  {
    listeners_.push_back(listener);
  }

  void removeListener(CustomSubjectInterface::Listener* listener) override
  {
    listeners_.erase(std::remove(listeners_.begin(), listeners_.end(), listener),listeners_.end());
  }

  void notifyListeners(CustomSubjectInterface* subject) override
  {
    for (CustomSubjectInterface::Listener* listener : listeners_) {
      listener->onUpdated(subject);
    }
  }
  
  void toggle() {
    if(isTrue_) {
      isTrue_ = false;
    } else {
      isTrue_ = true;
    }
    notifyListeners(this);
  }
  
  bool& getIsTrue() {
    return isTrue_;
  }
  
  void addMessage(juce::StringRef m, StatusCode code) {
    message = m;
    code_ = code;
  }
  
  std::string getMessage() {
    return message;
  }
  
  StatusCode getStatusCode() {
    return code_;
  }
  
  void setNewVoicingId(int newId) {
    newVoicingId = newId;
  }
  
  int getNewVoicingId() {
    return newVoicingId;
  }
  
private:
  int newVoicingId = 0;
  StatusCode code_ = Success;
  bool isTrue_ = false;
  std::string message;
  std::vector<CustomSubjectInterface::Listener*> listeners_;
};

Listener側では以下のコードで変更を受け取れます。

void onUpdated(CustomSubjectInterface* subject) override {
  BoolSubject* boolSubject = dynamic_cast<BoolSubject*>(subject);
  if (boolSubject != nullptr) {
    // Do something with the boolSubject
    bool isTrue = boolSubject->getIsTrue();
    // ...
  }
}
カテゴリー
C++ JUCE

JUCE SVG Button

参考にした情報

SVGアイコンをボタンに使うには以下のようにします。

addAndMakeVisible(velocityButton);
juce::Path velocityPath;
velocityPath.loadPathFromData(velocityPathData, sizeof(velocityPathData));

// if you want any transformation, use AffineTransform  velocityPath.applyTransform(juce::AffineTransform::rotation(juce::MathConstants<float>::halfPi));
    
velocityButton.setShape(velocityPath, true, true, false);
// set it to behave as a toggle button
velocityButton.setClickingTogglesState(true);
velocityButton.shouldUseOnColours(true);

AudioProcessorValueTreeState (APVTS)のButtonAttachmentを使うことでAPVTSパラメータとボタンが紐付けられます。

velocityButtonAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(valueTreeState, isVelocityRandomIdString, velocityButton);
カテゴリー
C++ JUCE

Message Thread

他のプログラミング言語、フレームワークと同様に、UIに関わる変更は、message thread (main thread)で行われる必要があります。

特に、AudioParameterの変更(background threadで処理されるようです)をトリガーとしてUIに変更を加える場合は、juce::CallbackMessageクラスを使って処理をmessage threadにdispatchしないと上手く動作しなかったり、Pluginがクラッシュします。

class ModulesContainerComponent
{
public:
  ModulesContainerComponent()
  {} 
  // 省略
  
  void parameterChanged(const juce::String &parameterID, float newValue) override {
    // dispatching to the message thread
    (new ResizeByAudioParameterCallback(*this))->post();
  }
  
  void updateControlBound() {
    lowerBound = (int)*valueTreeState.getRawParameterValue(LOWER_CONTROL_BOUND_ID);
    upperBound = (int)*valueTreeState.getRawParameterValue(UPPER_CONTROL_BOUND_ID);
  }
  
  void rebuild() {
    updateControlBound();
    int moduleCount = upperBound - lowerBound;
    // clear modules and rebuild
    modules.clear();
    if(moduleCount > 0) {
      for (int i = lowerBound; i <= upperBound; i++) {
        addAndMakeVisible(modules.add(std::make_unique<PerKeyModuleComponent>(i, valueTreeState, presetManager, lookAndFeel, voicings, dirtyVoices, colorHandler, showVoicingConfig)));
      }
    }
    repaint();
    resized();
  }
  
  // for dispatching to the message thread
  class ResizeByAudioParameterCallback : public juce::CallbackMessage
  {
  public:
    ResizeByAudioParameterCallback(ModulesContainerComponent& o)
      : owner(o)
    {
    
    }
    ModulesContainerComponent& owner;
    
    void messageCallback() override {
      owner.rebuild();
    }
  };
  
private:
  // 省略

  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ModulesContainerComponent)
};
カテゴリー
JUCE

Projucer build & distribution

JUCEのビルドシステム、Projucerの設定項目例 (Code-Signing: Automatic, Sandbox options: ユーザー選択ファイルのread/writeの例)

Xcode (macOS)

  • Use App Sandbox: Enabled
  • App Sandbox Options
    • File Access:: User Selected File (Read/Write)
    • Network: Outgoing Connections (Client)
  • Development Team ID: Your team ID (10 characters)

Debug and Release

  • Name: Debug or Lelease
  • Binary Name: Your plug-in name
  • Custom Xcode Flags
    • CODE_SIGN_STYLE=”Automatic”
  • Cod-Signing Identity: Apple Development

Plugin FormatsにStandaloneが含まれていないとconsole outされない&AllをビルドしないとUI関連のUpdateが反映されないという現象があります。

Plugin AU Main TypeはMidi Pluginの場合、”kAudioUnitType_MIDIProcessor”を選択、

Plugin CharacteristicsはMidi Pluginの場合、”Plugin MIDI Input”と”Plugin MIDI Output”を選択します。”MIDI Effect Plugin”にチェックを入れるとAbleton Liveではうまく表示されませんでした。

Distributionには公式Tutorialにもあるように Packagesを使います。

参考Youtube

Packagesを立ち上げ、Nextをクリック
Project Nameを記入し、この`Packages Project`のフォルダをどこに作成するか、を指定。Desktopでいいと思います。
このような画面が表示されます。
今回はAUとVST版のインストーラーを作るので、Packagesの下の「+」ボタンを押して追加、名前をそれぞれ「AU」「VST」とします。名前の変更は長押しすると可能になります。
JUCEプロジェクトのbuildファイルはエイリアスなので、Show Originalで元ファイルを探し
Packagesプロジェクトフォルダにコピーします。

インストールディレクトリのPathは Macintosh HD/Library/Audio/Plug-Ins/ で
AUの場合はComponents
VST3の場合はVST3  を指定します。

Packagesには、デフォルトでは上記のディレクトリが表示されていないため、Library下に作成します。ディレクトリを右クリックするとAdd Folderを選択出来ます。

Componentsフォルダを右クリックし、Add Filesで
先ほどPackagesプロジェクトフォルダ内にコピーしたcomponentファイルを選択します。
AU
VST
プロジェクトをSaveし
Buildします。
問題がなければこのように表示されます。

Packagesでインストーラーに問題がある場合は、pkgbuildで手動で作る方法があります。参考

カテゴリー
C++ JUCE

JUCE AudioProcessorValueTreeState

JUCEの初歩tutorialシリーズではあまり取り上げられない内容ですが、Plug-Inの色々なパラメータを管理するためにAudioProcessorValueTreeState(APVTS)はとても重要です。

APVTSを理解するにはまず、APVTSを使用しない場合どのようにパラメータを登録し、取り出して使うかを理解する必要があります。登録するにはAudioProcessorのconstructorでイニシャライズし、使う時はgetParameters( )でパラメータのポインタのリストをゲットします。

#define LOWER_MIDI_BOUND_ID "lowerMidiBound"

{
  addParameter(new juce::AudioParameterInt(juce::ParameterID{LOWER_MIDI_BOUND_ID, 1}, LOWER_MIDI_BOUND, 21, 108, 21)); 

}

auto& parameters = getParameters();
float gain = parameters[0]->getValue();

この方法では数個のパラメータでは管理出来るかも知れませんが、パラメータ数が膨大になると管理が難しくなります。そこでAPVTSの出番です。

APVTSはPluginProcessorのprivate変数として宣言し、constructorでイニシャライズします。

// initializer
AudioProcessorValueTreeState (
  AudioProcessor& processorToConnectTo,
  UndoManager* undoManagerToUse,
  const juce::Identifier& valueTreeType,
  ParameterLayout parameterLayout);
// PluginProcessor.h
class ChordGeniusAudioProcessor: public juce::AudioProcessor 
{
public:

  // 省略

  juce::AudioProcessorValueTreeState& getValueTreeState() { return valueTreeState; }

private:
  juce::AudioProcessorValueTreeState valueTreeState;
}

// PluginProcessor.cpp
// constructor
ChordGeniusAudioProcessor::ChordGeniusAudioProcessor()
  : // 省略
    valueTreeState(*this, nullptr, ProjectInfo::projectName, ParameterHelper::createParameterLayout())
{}

ParameterLayoutを供給するHelperクラスを作っておけば便利です。

#define LOWER_CONTROL_ID "LOWER_CONTROL_ID"

class ParameterHelper
{
public:
  ParameterHelper() = delete;

  static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout()
  {
    juce::AudioProcessorValueTreeState::ParameterLayout layout;
   // ParameterIDs can't contain spaces
    
    auto lowerId = std::make_unique<juce::AudioParameterInt>(juce::ParameterID{LOWER_CONTROL_ID, 1}, LOWER_CONTROL_ID, 21, 108, 21);
    
    layout.add(std::move(lowerId));
    return layout;
  }
};

公式Tutorialによると、APVTSはひとつのAudioProcessorにのみattach出来る、とあります。また、APVTSにparameterを追加すると自動的にAudioProcessorに追加したことになる、とあります。

また公式Tutorial動画によると、AudioParameterとAPVTSのAttachmentクラスを使用することで、Audio ThreadとUI Thread間のThread Safeなプログラムを書くことが出来る、とあります。SliderインスタンスはSliderAttachmentインスタンスよりもあとにdeconstructされる必要があるため、宣言の順番は重要です。

// AudioProcessorEditor.h

private:
  // 省略
  juce::AudioProcessorValueTreeState& valueTreeState;
  juce::Slider slider;

std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> sliderAttachment;

// AudioProcessorEditor.cpp
// constructor 
 : valueTreeState(p.getValueTreeState())
{
  sliderAttachment = std::make_unique<juce::AudioProcessorValueState::SliderAttachment>(valueTreeState, LOWER_CONTROL_ID, slider);
}

ちなみに、公式TutorialではJUCEでは全てのAudioParameterは[0, 1]のレンジで保存される、とあります。JUCE stores all of the parameter values in the range [0, 1] as this is a limitation of some of the target plug-in APIs. 

Host(DAW)がプロジェクトをsave/loadする時は、Plug-Inの状態(state)もsave/loadします。その際、Plug-Inのstateのload時はHostが管理するメモリ領域へserialize、save時はメモリ領域からdeserializeされます。

プロジェクトをsaveする時、Hostが getStateInformation (juce::MemoryBlock& destData)をコールし、Plug-Inのstateをゲットします。

void ChordGeniusAudioProcessor::getStateInformation(juce::MemoryBlock& destData) {
  // copying State from apvts, then create XML from it.
  if (auto xmlState = valueTreeState.copyState().createXml()) 
  {
    // dereference from xmlState and copying it to MemoryBlock
    // human readable xml will allow debugging...
    copyXmlToBinary(*xmlState, destData);
  }
}

プロジェクトをloadする時、Hostが setStateInformation (const void* data, int sizeInByte)をコールしします。

void ChordGeniusAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
  if(auto xmlState = getXmlFromBinary (data, sizeInBytes)
  {
    if (xmlState->hasTagName(valueTreeState.state.getType())) {
    const auto newTree = juce::ValueTree::fromXml(*xmlState);
    valueTreeState.replaceState(newTree);
    }
  }
}

APVTSからパラメータをゲットする時は

// returns juce::Value type object
auto param = valueTreeState.getParameterAsValue("PARAMETER_ID");

パラメータに値をセットするときは

// need to cast to juce::var type in order for Host application to save and persist the plut-in state.

param.setValue(juce::var(newValue));
カテゴリー
C++ JUCE

JUCEのPopupMenu

JUCEのComboBoxのPopupMenuがGUIの後ろに隠れてしまい、on topに表示されない問題がある場合、この情報をもとにLookAndFeelのメソッドをoverrideすると、うまくon topに表示出来て、またドラッグ動作の際 parent componentと一緒に動く挙動を実装出来ます。

Component* getParentComponentForMenuOptions (const PopupMenu::Options& options) override
{
    if (auto* comp = options.getParentComponent())
        return comp;

    if (auto* targetComp = options.getTargetComponent())
    {
        if (auto* editor = targetComp->findParentComponentOfClass<AudioProcessorEditor>())
            return editor;
    }

    return LookAndFeel_V4::getParentComponentForMenuOptions (options);
}

AudioProcessorEditorをparentとすることで、ComboBoxのpopupが全てのWindowよりもfrontに来るように出来ます。

カテゴリー
iOS Swift

iOS & Swift 102 Threadについて

これまで見てきたコードは、全体がひとつの流れになっていて、初めから終わりまでが順番に処理されていくものになっていました。

このひとつの流れのことをThread(スレッド)といいます。

以下の例はボタンをタップすると1秒ごとにコンソールに変数iの値がプリントされるプログラムです。

import UIKit

class ViewController: UIViewController {
  
  lazy var button: UIButton = {
    let button = UIButton()
    button.setTitle("Tap", for: .normal)
    button.setTitleColor(.white, for: .normal)
    button.backgroundColor = .red
    button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside)
    return button
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.addSubview(button)
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    button.frame = CGRect(x: view.bounds.width/2 - 50, y: view.bounds.height/2 - 50, width: 100, height: 100)
  }
  
  private func loop () {
    for i in 0..<100 {
      Thread.sleep(forTimeInterval: 1)
      print(Thread.current)
      print(i)
    }
  }
  
  @objc private func onButtonTapped() {
    loop()
    print("button tapped")
  }
}

これをビルドし、ボタンをタップすると、コンソールに1秒ごとに変数iのカウントがプリントされます。

[1] 現在のThreadオブジェクトをコンソールにプリントしています。<_NSMainThread: ….>とプリントされているのがメイン・スレッドです。

Swiftだけではなく、他の色々なプログラミング言語・OSにおいても同じですが、UI(ユーザーインターフェース)はmain thread(メイン・スレッド)で動作しています。

カウント中にボタンをタップしても、コンソールには”button tapped”とプリントされないことに気が付くと思います。

これはThread.sleepがメイン・スレッドにおいて行われているため、UIがブロック(フリーズ)しているからです。

プログラミングでは、UIが動作するメインスレッドでは時間のかかるタスクを処理させてはいけないという大前提があります。理論上プログラムはクラッシュせずに正しく動いているのですが、UIが固まって見えるようでは、ユーザー満足度はとても低くなるからです。時間のかかるタスクはUIとは別のスレッド(バックグラウンド・スレッド)で処理する必要があります。

実際のアプリではひとつのThreadだけで動作していることはあまりなく、複数のThreadが起動しています。これをMulti Thread(マルチ・スレッド)といいます。

iOSではこのマルチ・スレッドを簡単に取り扱えるGrand Central Dispatch(GCD)という仕組みがあります。

loop( )関数を以下のように書き換えてみましょう。

private func loop () {
  // バックグラウンド・キュー(Thread)を作成
  DispatchQueue.global().async {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
    // NS Thread number = 5 などとプリントされる
      print(Thread.current)
      print(i)        
    }
  }
}

ボタンをタップするとバックグラウンド・スレッドが起動され、そのすぐ後に”button tapped”とプリントされ、カウントが始まるのがわかると思います。

もう一度ボタンをタップすると、また”button tapped”とプリントされ、先ほどとは違うカウントが「並列(concurrent)に」実行されているのがわかると思います。これは、もうひとつ別のバックグラウンド・スレッドを起動した、ということです。

次に、ひとつラベルを用意して、変数iの値をスクリーンに表示するようにしてみましょう。太字部分が先ほどのコードからの変更点です。

class ViewController: UIViewController {
  
  // labelを用意
  lazy var label: UILabel = {
    let label = UILabel()
    label.backgroundColor = .blue
    label.textColor = .white
    label.textAlignment = .center
    return label
  }()
  
  lazy var button: UIButton = {
    let button = UIButton()
    button.setTitle("On / Off", for: .normal)
    button.setTitleColor(.white, for: .normal)
    button.backgroundColor = .red
    button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside)
    return button
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.addSubview(button)
  // labelをviewに追加
    view.addSubview(label)
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    button.frame = CGRect(x: view.bounds.width/2 - 50, y: view.bounds.height/2 - 50, width: 100, height: 100)
  // labelをレイアウト
    label.frame = CGRect(x: view.bounds.width/2 - 50, y: view.bounds.height/4 - 25, width: 100, height: 50)
  }
  
  
  private func loop () {
    DispatchQueue.global().async {
      for i in 0..<10 {
        Thread.sleep(forTimeInterval: 1)
        print(Thread.current)
     // labelテキストにiを代入
        self.label.text = "\(i)"
      }
    }
  }
  
  @objc private func onButtonTapped() {
    loop()
    print("button tapped")
  }
}

ビルドし、ボタンをタップしてみましょう。

実はこのコードはうまく動作しません。

UI関連のアップデートはバックグラウンド・スレッドから行ってはいけないというルールがあるからです。

loop( )関数を以下のように変えるとうまく動作します。

private func loop () {
  DispatchQueue.global().async {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
      print(Thread.current)
      // バックグラウンド・スレッドから
    // メイン・スレッドに処理を戻している
      DispatchQueue.main.async {
        self.label.text = "\(i)"          
      }
    }
  }
}
カテゴリー
iOS Swift

iOS & Swift 102 callbackとdelegate

処理の中には、一定の時間がかかるものがあり(インターネット越しにあるAPIサーバーからレスポンスを待つ、など)、その処理が終わった後にプログラムに「これをやっといてね」と「頼み」たいことがよくあります。一般的にはそれらの処理を行う関数をcallback関数と呼びますが、Swiftではclosureを使ったパターン、delegateを使ったパターンがよく見られます。

まず closureで実装した例を説明します。

// PlaySound.swift

class PlaySound {

  // [1] 処理に1秒かかる作業を10回実行しているシミュレーション。1秒ごとにcallbackを呼ぶ。callbackはclosure型で指定
  public func play(callback: (Int) -> Void) {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
      callback(i)
    }
  }
}

// ViewController.swift

class ViewController: UIViewController {
 // PlaySoundクラスのインスタンス
  let playSound = PlaySound()

  override func viewDidLoad() {
    super.viewDidLoad()
    // [2] インスタンスメソッドをコールする時に関数を渡している
    playSound.play(callback: printNum)
  }
  
  // コールバック関数の実装をここで記述出来る
  private func printNum(num: Int) {
    print(num)
  }
}

[1] 関数 play() はclosure型の引数を取ります。

closureとは、名前の無い関数のことで、以下のように( ) -> ( )のような形で書くことができます。最初の( )にはそのclosureの引数を、->(矢印)の後の( )には返り値を記述します。

( ) はtuple(タプル)という型で、複数の異なる型を入れられます。

ここで面白いのは、play( )関数の引数として渡すclosureの型は指定しますが、closureでどのような処理をするのかは、play( )関数を定義する時には決めなくてもよく、その関数を呼ぶ時に決められる、ということです。

[2] 上記の例ではplay( )に渡すためのprintNum(num: Int)という関数をViewControllerクラスで定義し、play(callback: printNum)という形で渡しています。

(Int) -> Void というclosureの型と printNum(num: Int)は合っている必要があります。

func printNum(num: Int) -> Void { // 返り値がない場合 -> Voidは省略可

}
// closureの書き方の例

// 引数なし、返り値なし
() -> ()
() -> Void

// 引数はInt型、返り値はなし
(Int) -> ()
(Int) -> Void

// 引数はIntとFloat、返り値はIntとBool
(Int, Float) -> (Int, Bool)

次に、Delegateパターンの説明をしますが、その前にProtocolについて理解する必要があります。

Protocolとは、この処理を担当するなら、これらの関数(や変数)は備えといてね、という「規約」を決めるものです。

// PlaySound.swift

// [1] Protocolを定義
protocol PlaySoundDelegate {
  func printNum(num: Int)
}

class PlaySound {

 // [1] Protocol型の変数を宣言
  var playDelegate: PlaySoundDelegate!

  public func play() {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
   // ここでProtocolの関数を使う
      playSoundDelegate.printNum(num: i)
    }
  }
}

// ViewController.swift

class ViewController: UIViewController,
                        PlaySoundDelegate // [2]-b   Protocolに準拠
{

  let playSound = PlaySound()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // [2]-a   delegateをself(ViewControllerクラス)に指定
    playSound.playSoundDelegate = self
    playSound.play()
  
  }
  // [2]-c   Protocolの関数を定義
  func printNum(num: Int) {
    print(num)
  }
}

[1] delegateは「任せる」という意味です。PlaySoundクラスの色々な処理が完了したら、その次に任せたい処理(関数)をProtocolに記述します。Protocolには関数の宣言だけをし、定義はProtocolに準拠するクラスに記述します。

[2] 上記の例ではViewControllerクラスをPlaySoundDelegate Protocolに準拠させることにします。

[2]-a playSound.playSoundDelegate = self はViewControllerクラスがdelegateを担当しますよ、と指定しています。

[2]-b その際、クラスがProtocolに準拠していないとエラーになります。

[2]-c また、Protocolの必須関数を定義していないとエラーになります。

どちらの実装方法でも同じようなことは実現出来るので、まずは一つのやり方にじっくり慣れると、より理解が深まると思います。