web-dev-qa-db-ja.com

UISplitViewControllerと他のViewControllerを切り替える最良の方法は?

IPadアプリを作成しています。アプリの画面の1つは、UISplitViewControllerの使用に完全に適しています。ただし、アプリのトップレベルはメインメニューであり、UISplitViewControllerを使用したくありません。 Appleは次のように述べているため、これには問題があります。

  1. UISplitViewControllerはアプリのトップレベルのビューコントローラーである必要があります。つまり、そのビューはUIWindowのサブビューとして追加する必要があります。

  2. 使用する場合、UISplitViewControllerはアプリの存続期間中存在する必要があります。つまり、UIWindowからビューを削除して別のビューを配置したり、その逆を行ったりしないでください。

読んで実験した結果、Appleの要件を満たすのは実行可能な選択肢にすぎないようで、私たち自身はモーダルダイアログを使用することです。そのため、アプリのルートレベルにUISplitViewControllerがあり(つまり、そのビューがUIWindowのサブビューとして追加されています)、メインメニューを表示するために、フルスクリーンのモーダルダイアログとしてUISplitViewControllerにプッシュします。次に、メインメニュービューコントローラーのモーダルダイアログを閉じることで、分割ビューを実際に表示できます。

この戦略はうまく機能しているようです。しかし、それは疑問を投げかけます:

1)モーダルなしで、前述のすべての要件も満たす、これを構造化するためのより良い方法はありますか?モーダルダイアログとしてプッシュされるため、メインUIが表示されるのは少し奇妙に思えます。 (モーダルは、焦点を絞ったユーザータスク用であると想定されています。)

2)私のアプローチのためにアプリストアが拒否されるリスクがありますか?このモーダル戦略は、Appleのヒューマンインターフェイスガイドラインに従って、おそらく「誤用」モーダルダイアログです。しかし、彼らは私に他にどのような選択肢を与えましたか?とにかく、彼らは私がこれをしていることを知っていますか?

28
occulus

Touche!同じ問題に遭遇し、モーダルを使用して同じ方法で解決しました。私の場合、分割ビューの前に表示されるのはログインビューであり、メインメニューでもありました。私はあなたが考えたのと同じ戦略を使いました。私(および私が話した他のいくつかの知識のあるiOSの人々)は、より良い方法を見つけることができませんでした。私にとってはうまくいきます。とにかく、ユーザーはモーダルに気付くことはありません。それらを提示します。そして、はい、AppStoreには内部で同じことをしているアプリがかなりあることもわかります。 :)別の注意点として、いつかどうにかしてもっと良い方法を見つけたら教えてください:)

6
Bourne

UISplitViewControllerの前にいくつかのUIViewControllerを表示するというこの概念(ログインフォームなど)は、必要になるまで、非常に複雑であることが判明したと真剣に信じていませんでした。そのようなビュー階層を作成します。

私の例はiOS8とXCode6.0(Swift)に基づいているので、この問題が以前に同じように存在したかどうか、またはiOS8で導入​​されたいくつかの新しいバグが原因であるかどうかはわかりません、しかし私が見つけた同様の質問のすべてから、私はこの問題に対する完全な「それほどハッキーではない」解決策を見ませんでした。

私が解決策にたどり着く前に私が試したいくつかのことをあなたに案内します(この投稿の終わりに)。各例は、CoreDataを有効にせずにMaster-Detailテンプレートから新しいプロジェクトを作成することに基づいています。


最初の試行(UISplitViewControllerへのモーダルセグエ):

  1. 新しいUIViewControllerサブクラスを作成します(たとえば、LoginViewController)
  2. ストーリーボードに新しいビューコントローラーを追加し、(UISplitViewControllerではなく)初期ビューコントローラーとして設定して、LoginViewControllerに接続します
  3. loginViewControllerにUIButtonを追加し、そのボタンからUISplitViewControllerへのモーダルセグエを作成します
  4. uISplitViewControllerのボイラープレートセットアップコードをAppDelegateのdidFinishLaunchingWithOptionsからLoginViewControllerのprepareForSegueに移動します

これはほとんど機能しました。アプリがLoginViewControllerで起動し、ボタンをタップしてUISplitViewControllerにセグエした後、奇妙なバグが発生しているため、ほぼ言います:向きの変更時にマスタービューコントローラーを表示および非表示にすることはアニメーション化されなくなりました。

この問題にしばらく苦労し、実際の解決策がなかった後、私はそれが何らかの形でそれに関連していると思いました奇妙なルール UISplitViewControllerはrootViewControllerでなければなりません(この場合はそうではありません、LoginViewControllerはそうです)これはそれほど完璧ではない解決策です。


2回目の試行(UISplitViewControllerからのモーダルセグエ):

  1. 新しいUIViewControllerサブクラスを作成します(たとえば、LoginViewController)
  2. ストーリーボードに新しいViewControllerを追加し、LoginViewControllerに接続します(ただし、今回はUISplitViewControllerを最初のView Controllerのままにします)
  3. uISplitViewControllerからLoginViewControllerへのモーダルセグエを作成します
  4. loginViewControllerにUIButtonを追加し、そのボタンからアンワインドセグエを作成します

最後に、UISplitViewControllerを設定するための定型コードの後に​​、このコードをAppDelegateのdidFinishLaunchingWithOptionsに追加します。

window?.makeKeyAndVisible()
splitViewController.performSegueWithIdentifier("segueToLogin", sender: self)
return true

または、代わりに次のコードを試してください。

window?.makeKeyAndVisible()
let loginViewController = splitViewController.storyboard?.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
splitViewController.presentViewController(loginViewController, animated: false, completion: nil)
return true

これらの例は両方とも、同じいくつかの悪いことを生み出します。

  1. コンソール出力:Unbalanced calls to begin/end appearance transitions for <UISplitViewController: 0x7fc8e872fc00>
  2. LoginViewControllerがモーダルにセグエされる前に、UISplitViewControllerを最初に表示する必要があります(ログイン前にUISplitViewControllerが表示されないように、ログインフォームのみを表示したい)
  3. アンワインドセグエは呼び出されません(これはまったく別のバグであり、今はその話には入りません)

解決策(rootViewControllerを更新)

正しく機能することがわかった唯一の方法は、ウィンドウのrootViewControllerをその場で変更する場合です。

  1. LoginViewControllerとUISplitViewControllerのストーリーボードIDを定義し、AppDelegateにある種のloggedInプロパティを追加します。
  2. このプロパティに基づいて、適切なView Controllerをインスタンス化し、その後、rootViewControllerとして設定します。
  3. didFinishLaunchingWithOptionsでアニメーションなしで実行しますが、UIから呼び出されるとアニメーション化されます。

AppDelegateのサンプルコードは次のとおりです。

var loggedIn = false

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    setupRootViewController(false)
    return true
}

func setupRootViewController(animated: Bool) {
    if let window = self.window {
        var newRootViewController: UIViewController? = nil
        var transition: UIViewAnimationOptions

        // create and setup appropriate rootViewController
        if !loggedIn {
            let loginViewController = window.rootViewController?.storyboard?.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
            newRootViewController = loginViewController
            transition = .TransitionFlipFromLeft

        } else {
            let splitViewController = window.rootViewController?.storyboard?.instantiateViewControllerWithIdentifier("SplitVC") as UISplitViewController
            let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as UINavigationController
            navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
            splitViewController.delegate = self

            let masterNavigationController = splitViewController.viewControllers[0] as UINavigationController
            let controller = masterNavigationController.topViewController as MasterViewController

            newRootViewController = splitViewController
            transition = .TransitionFlipFromRight
        }

        // update app's rootViewController
        if let rootVC = newRootViewController {
            if animated {
                UIView.transitionWithView(window, duration: 0.5, options: transition, animations: { () -> Void in
                    window.rootViewController = rootVC
                    }, completion: nil)
            } else {
                window.rootViewController = rootVC
            }
        }
    }
}

そしてこれはLoginViewControllerからのサンプルコードです:

@IBAction func login(sender: UIButton) {
    let delegate = UIApplication.sharedApplication().delegate as AppDelegate
    delegate.loggedIn = true
    delegate.setupRootViewController(true)
}

iOS 8でこれが正しく機能するためのより良い/よりクリーンな方法があるかどうかも聞きたいです。

19
tadija

そして、誰があなたが1つのウィンドウしか持つことができないと言いましたか? :)

私の答え この同様の質問について が役立つかどうかを確認してください。

このアプローチは私にとって非常にうまく機能しています。複数の表示や状態の復元について心配する必要がない限り、このリンクされたコードは必要なことを行うのに十分なはずです。ロジックを逆向きにしたり、既存のコードを書き直したりする必要はなく、それでも利用できます。アプリケーション内のより深いレベルでのUISplitViewの-(AFAIK)を破ることなくAppleガイドライン。

3

同じ問題に直面している将来のiOS開発者のために:ここに別の答えと説明があります。あなたはそれをルートビューコントローラーにする必要があります。そうでない場合は、モーダルをオーバーレイします。

ルートビューコントローラーとしてではなくUISplitviewコントローラー

1
Krumelur

プロジェクトでこの問題に遭遇し、解決策を共有したいと思いました。私たちの場合(iPadの場合)、両方のビューコントローラーが表示された状態でUISplitViewControllerから始めたいと思いました(preferredDisplayMode = .allVisibleを使用)。詳細(右)階層のある時点で(こちら側にもナビゲーションコントローラーがありました)、分割ビューコントローラー全体に新しいビューコントローラーをプッシュしたいと思いました(モーダルトランジションは使用しません)。

IPhoneでは、この動作は無料で提供されます。常に1つのViewControllerしか表示されないためです。しかし、iPadでは、何か別のことを理解する必要がありました。最終的に、分割ビューコントローラーを子ビューコントローラーとして追加するルートコンテナービューコントローラーを使用することになりました。このルートビューコントローラーは、ナビゲーションコントローラーに組み込まれています。分割ビューコントローラの詳細ビューコントローラが分割ビューコントローラ全体に新しいコントローラをプッシュする場合、ルートビューコントローラはこの新しいビューコントローラをナビゲーションコントローラとともにプッシュします。

1
Evan R

別のオプション:詳細ビューコントローラーに、モーダルビューコントローラーを表示します。

let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
if (!appDelegate.loggedIn) {
    // display the login form
    let storyboard = UIStoryboard(name: "Storyboard", bundle: nil)
    let login = storyboard.instantiateViewControllerWithIdentifier("LoginViewController") as UIViewController
    self.presentViewController(login, animated: false, completion: { () -> Void in
       // user logged in and is valid now
       self.updateDisplay()
    })
} else {
    updateDisplay()
}

ログインフラグを設定せずにログインコントローラを閉じないでください。 iPhoneではマスタービューコントローラーが最初に来るので、非常によく似たコードがマスタービューコントローラーにある必要があることに注意してください。

0
elcuco

-presentViewController:animated:completion:を介して、UISplitViewControllerを提示するための私のアプローチに貢献したいと思います(ただし、それが機能しないことは誰もが知っています)。以下に応答するUISplitViewControllerサブクラスを作成しました。

-presentAsRootViewController
-returnToPreviousViewController

このクラスは、他の成功したアプローチと同様に、UISplitViewControllerをウィンドウのrootViewControllerとして設定しますが、-presentViewController:animated:completion:で(デフォルトで)取得するものと同様のアニメーションで設定します。

PresentableSplitViewController.h

#import <UIKit/UIKit.h>    
@interface PresentableSplitViewController : UISplitViewController    
- (void) presentAsRootViewController;
@end

PresentableSplitViewController.m

#import "PresentableSplitViewController.h"

@interface PresentableSplitViewController ()
@property (nonatomic, strong) UIViewController *previousViewController;
@end

@implementation PresentableSplitViewController

- (void) presentAsRootViewController {

    UIWindow *window=[[[UIApplication sharedApplication] delegate] window];
    _previousViewController=window.rootViewController;

    UIView *windowSnapShot = [window snapshotViewAfterScreenUpdates:YES];
    window.rootViewController = self;

    [window insertSubview:windowSnapShot atIndex:0];

    CGRect dstFrame=self.view.frame;

    CGSize offset=CGSizeApplyAffineTransform(CGSizeMake(0, 1), window.rootViewController.view.transform);
    offset.width*=self.view.frame.size.width;
    offset.height*=self.view.frame.size.height;
    self.view.frame=CGRectOffset(self.view.frame, offset.width, offset.height);

    [UIView animateWithDuration:0.5
                          delay:0.0
         usingSpringWithDamping:1.0
          initialSpringVelocity:0.0
                        options:UIViewAnimationOptionCurveEaseInOut
                     animations:^{
                         self.view.frame=dstFrame;
                     } completion:^(BOOL finished) {
                         [windowSnapShot removeFromSuperview];
                     }];
}

- (void) returnToPreviousViewController {
    if(_previousViewController) {

        UIWindow *window=[[[UIApplication sharedApplication] delegate] window];

        UIView *windowSnapShot = [window snapshotViewAfterScreenUpdates:YES];
        window.rootViewController = _previousViewController;

        [window addSubview:windowSnapShot];

        CGSize offset=CGSizeApplyAffineTransform(CGSizeMake(0, 1), window.rootViewController.view.transform);
        offset.width*=windowSnapShot.frame.size.width;
        offset.height*=windowSnapShot.frame.size.height;

        CGRect dstFrame=CGRectOffset(windowSnapShot.frame, offset.width, offset.height);

        [UIView animateWithDuration:0.5
                              delay:0.0
             usingSpringWithDamping:1.0
              initialSpringVelocity:0.0
                            options:UIViewAnimationOptionCurveEaseInOut
                         animations:^{
                             windowSnapShot.frame=dstFrame;
                         } completion:^(BOOL finished) {
                             [windowSnapShot removeFromSuperview];
                             _previousViewController=nil;
                         }];
    }
}

@end
0
hyperspasm

UISplitViewを初期ビューとして実行しましたが、モーダルでフルスクリーンUIViewに移動し、UISplitViewに戻りました。 SplitViewに戻る必要がある場合は、カスタムセグエを使用する必要があります。

このリンクを読む(日本語から翻訳)

IViewControllerからUISplitViewController

0
Gil Beyruth

@tadijaの答えに加えて、私は同様の状況にあります:

私のアプリは電話専用で、タブレットUIを追加しています。 Swift同じアプリで-そして最終的にはすべてのアプリを移行して同じストーリーボードを使用することにしました(IPadバージョンが安定していると感じたら、電話での使用は簡単です。 XCode6からの新しいクラス)。

私のシーンではまだセグエが定義されていませんが、それでも機能します。

アプリデリゲートのコードはObjectiveCにあり、少し異なりますが、同じアイデアを使用しています。前の例とは異なり、シーンのデフォルトのViewControllerを使用していることに注意してください。これは、ランタイムがUINavigationControllerではなく通常のUISplitViewControllerを生成するIOS7/IPhoneでも機能すると思います。 rootVCを変更する代わりに、iPhoneのログインビューコントローラーをプッシュする新しいコードを追加することもできます。

- (void) setupRootViewController:(BOOL) animated {
    UIViewController *newController = nil;
    UIStoryboard *board = [UIStoryboard storyboardWithName:@"Storyboard" bundle:nil];
    UIViewAnimationOptions transition = UIViewAnimationOptionTransitionCrossDissolve;

    if (!loggedIn) {
        newController = [board instantiateViewControllerWithIdentifier:@"LoginViewController"];
    } else {
        newController = [board instantiateInitialViewController];
    }

    if (animated) {
        [UIView transitionWithView: self.window duration:0.5 options:transition animations:^{
            self.window.rootViewController = newController;
            NSLog(@"setup root view controller animated");
        } completion:^(BOOL finished) {
            NSLog(@"setup root view controller finished");
        }];
    } else {
        self.window.rootViewController = newController;
    }
}
0
elcuco