web-dev-qa-db-ja.com

Mobile SafariタブのようなUIScrollView水平ページング

Mobile Safariでは、下部にページコントロールがあるUIScrollView水平ページングビューのようなものを入力することにより、ページを切り替えることができます。

水平スクロール可能なUIScrollViewが次のビューのコンテンツの一部を表示するこの特定の動作を再現しようとしています。

Appleの例:PageControlは、水平ページングにUIScrollViewを使用する方法を示していますが、すべてのビューが画面幅全体を占めています。

モバイルSafariのようにUIScrollViewを取得して次のビューのコンテンツを表示するにはどうすればよいですか?

88
firstresponder

ページングを有効にしたUIScrollViewは、そのフレーム幅(または高さ)の倍数で停止します。そのため、最初のステップは、ページの幅を決定することです。 UIScrollViewの幅を確認します。次に、サブビューのサイズを必要に応じて大きく設定し、UIScrollViewの幅の倍数に基づいて中心を設定します。

それから、もちろん他のページを見たいので、mhjoyが述べたようにclipsToBoundsNOに設定します。トリックの一部は、ユーザーがUIScrollViewのフレームの範囲外でドラッグを開始したときにスクロールさせることです。私の解決策(最近これをやらなければならなかったとき)は次のとおりでした。

UIViewとそのサブビューを含むClipViewサブクラス(つまり、UIScrollView)を作成します。基本的に、通常の状況でUIScrollViewが想定するフレームを持っている必要があります。 UIScrollViewClipViewの中央に配置します。幅が親ビューの幅より小さい場合は、ClipViewclipsToBoundsYESに設定されていることを確認してください。また、ClipViewにはUIScrollViewへの参照が必要です。

最後のステップは、ClipView内の- (UIView *)hitTest:withEvent:をオーバーライドすることです。

_- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  return [self pointInside:point withEvent:event] ? scrollView : nil;
}
_

これは基本的に、UIScrollViewのタッチ領域を、親のビューのフレームに、まさに必要なものに拡張します。

別のオプションはUIScrollViewをサブクラス化してその- (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventメソッドをオーバーライドすることですが、クリッピングを行うにはコンテナビューが必要であり、YESは、UIScrollViewのフレームのみに基づいています。

注:Juri Pakasteの hitTest:withEvent:modification もご覧くださいサブビューのユーザーインタラクションに問題があります。

260
Ed Marty

上記のClipViewソリューションはうまくいきましたが、別の-[UIView hitTest:withEvent:]実装。 Ed Martyのバージョンでは、ユーザーが水平スクロールビュー内にある垂直スクロールビューを操作できませんでした。

次のバージョンは私のために働いた:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}
69
Juri Pakaste

ScrollViewのフレームサイズをページサイズとして設定します。

[self.view addSubview:scrollView];
[self.view addGestureRecognizer:mainScrollView.panGestureRecognizer];

これで、self.view、およびscrollViewのコンテンツがスクロールされます。
また、scrollView.clipsToBounds = NO;コンテンツのクリッピングを防ぎます。

8

カスタムUIScrollViewを自分で使用することにしました。ただし、正確なコードが表示されなかったため、共有すると思いました。私のニーズは、小さなコンテンツを持つUIScrollViewであり、UIScrollView自体はページング効果を達成するには小さかったです。投稿にあるように、スワイプすることはできません。しかし、今はできます。

クラスCustomScrollViewとサブクラスUIScrollViewを作成します。次に、これを.mファイルに追加するだけです。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  return (point.y >= 0 && point.y <= self.frame.size.height);
}

これにより、左右にスクロールできます(水平)。それに応じて境界を調整し、スワイプ/スクロールタッチ領域を設定します。楽しい!

5
djneely

Scrollviewを自動的に返すことができる別の実装を作成しました。そのため、プロジェクトでの再利用を制限するIBOutletを用意する必要はありません。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self pointInside:point withEvent:event]) {
        for (id view in [self subviews]) {
            if ([view isKindOfClass:[UIScrollView class]]) {
                return view;
            }
        }
    }
    return nil;
}
4
alvinhu

ClipViewのhitTest実装に役立つ可能性のある別の変更があります。 ClipViewへのUIScrollView参照を提供する必要はありませんでした。以下の実装では、ClipViewクラスを再利用して、あらゆるもののヒットテスト領域を拡張し、参照を提供する必要はありません。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (([self pointInside:point withEvent:event]) && (self.subviews.count >= 1))
    {
        // An extended-hit view should only have one sub-view, or make sure the
        // first subview is the one you want to expand the hit test for.
        return [self.subviews objectAtIndex:0];
    }

    return nil;
}
3
Rod Reddekopp

このSO質問。読みやすいようにスクロールビューへの参照(self.scrollView)を使用します。

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = nil;
    NSArray *childViews = [self.scrollView subviews];
    for (NSUInteger i = 0; i < childViews.count; i++) {
        CGRect childFrame = [[childViews objectAtIndex:i] frame];
        CGRect scrollFrame = self.scrollView.frame;
        CGPoint contentOffset = self.scrollView.contentOffset;
        if (childFrame.Origin.x + scrollFrame.Origin.x < point.x + contentOffset.x &&
            point.x + contentOffset.x < childFrame.Origin.x + scrollFrame.Origin.x + childFrame.size.width &&
            childFrame.Origin.y + scrollFrame.Origin.y < point.y + contentOffset.y &&
            point.y + contentOffset.y < childFrame.Origin.y + scrollFrame.Origin.y + childFrame.size.height
        ){
            hitView = [childViews objectAtIndex:i];
            return hitView;
        }
    }
    hitView = [super hitTest:point withEvent:event];
    if (hitView == self)
        return self.scrollView;
    return hitView;
}

これを子ビューに追加して、タッチイベントをキャプチャします。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

(これはuser1856273のソリューションのバリエーションです。読みやすくするためにクリーンアップされ、Bartserkのバグ修正が組み込まれました。user1856273の回答を編集することを考えましたが、変更するには大きすぎました。)

3
David James

上記の提案を実装しましたが、使用していたUICollectionViewは、フレームの外にあるものはすべて画面外にあると考えました。これにより、近くのセルは、ユーザーがそれらに向かってスクロールしているときにのみ境界外にレンダリングされましたが、これは理想的ではありませんでした。

私がやったことは、以下のメソッドをデリゲート(またはUICollectionViewLayout)に追加することにより、スクロールビューの動作をエミュレートすることでした。

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{    
  if (velocity.x > 0) {
    proposedContentOffset.x = ceilf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }
  else {
    proposedContentOffset.x = floorf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }

  return proposedContentOffset;
}

これにより、スワイプアクションの委任が完全に回避され、これもボーナスでした。 UIScrollViewDelegateには、scrollViewWillEndDragging:withVelocity:targetContentOffset:と呼ばれる同様のメソッドがあり、UITableViewsおよびUIScrollViewsのページングに使用できます。

3
Kyle

私のバージョンは、スクロールの上にあるボタンを押す-仕事=)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView* child = nil;
    for (int i=0; i<[[[[self subviews] objectAtIndex:0] subviews] count];i++) {

        CGRect myframe =[[[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i] frame];
        CGRect myframeS =[[[self subviews] objectAtIndex:0] frame];
        CGPoint myscroll =[[[self subviews] objectAtIndex:0] contentOffset];
        if (myframe.Origin.x < point.x && point.x < myframe.Origin.x+myframe.size.width &&
            myframe.Origin.y+myframeS.Origin.y < point.y+myscroll.y && point.y+myscroll.y < myframe.Origin.y+myframeS.Origin.y +myframe.size.height){
            child = [[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i];
            return child;
        }


    }

    child = [super hitTest:point withEvent:event];
    if (child == self)
        return [[self subviews] objectAtIndex:0];
    return child;
    }

ただし、[[self subviews] objectAtIndex:0]のみがスクロールである必要があります

2
kolbasek

エド・マーティの回答に基づいたSwift回答ですが、スクロールビュー内でボタンをタップできるようにするためのJuri Pakasteによる修正も含まれています。

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}
0
Ben Packard