Xamarin Forms 1.3.4.6332 iOS ScrollViewRenderer 내비게이션 이슈

문제점

Xamarin Forms iOS ScrollViewRenderer가 1.3.4.6332 버전에서 NavigationPage.HasNavigationBarProperty 바인딩 속성이 true인 페에지에 포함되어 있으면 다른 페이지로 이동할 때 맨위로 스크롤되는 현상이 발견되었습니다. 호출 스택은 다음과 같습니다.

0x14 in UIKit.UIScrollView._UIScrollViewDelegate.Scrolled at /Developer/MonoTouch/Source/monotouch/src/build/native/UIKit/UIScrollView.g.cs:1074,6
0x25 in ObjCRuntime.Messaging.void_objc_msgSendSuper_CGSize
0x3C in UIKit.UIScrollView.set_ContentSize at /Developer/MonoTouch/Source/monotouch/src/build/native/UIKit/UIScrollView.g.cs:430,6
0xF in Xamarin.Forms.Platform.iOS.ScrollViewRenderer.OnNativeControlUpdated

0x47 in Xamarin.Forms.Platform.iOS.VisualElementTracker.UpdateNativeControl
0x15B in Xamarin.Forms.Platform.iOS.VisualElementTracker.HandlePropertyChanged
0x12 in Xamarin.Forms.BindableObject.OnPropertyChanged
0xFF in Xamarin.Forms.BindableObject.SetValueActual
0x23E in Xamarin.Forms.BindableObject.SetValueCore
0x75 in Xamarin.Forms.BindableObject.SetValue
0x18 in Xamarin.Forms.BindableObject.SetValue
0xC in Xamarin.Forms.VisualElement.set_Height
0x1C in Xamarin.Forms.VisualElement.SetSize
0x6C in Xamarin.Forms.VisualElement.set_Bounds
0x2 in Xamarin.Forms.VisualElement.Layout
0x118 in Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion
0x158 in Xamarin.Forms.Grid.LayoutChildren
0xC8 in Xamarin.Forms.Layout.UpdateChildrenLayout
0x10 in Xamarin.Forms.Layout.OnSizeAllocated
0x3 in Xamarin.Forms.VisualElement.SizeAllocated
0x24 in Xamarin.Forms.VisualElement.SetSize
0x6C in Xamarin.Forms.VisualElement.set_Bounds
0x2 in Xamarin.Forms.VisualElement.Layout
0x118 in Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion
0x105 in Xamarin.Forms.Page.LayoutChildren
0xAD in Xamarin.Forms.Page.UpdateChildrenLayout
0x10 in Xamarin.Forms.Page.OnSizeAllocated
0x3 in Xamarin.Forms.VisualElement.SizeAllocated
0x24 in Xamarin.Forms.VisualElement.SetSize
0x6C in Xamarin.Forms.VisualElement.set_Bounds
0x2 in Xamarin.Forms.VisualElement.Layout
0x118 in Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion
0x105 in Xamarin.Forms.Page.LayoutChildren
0xAD in Xamarin.Forms.Page.UpdateChildrenLayout
0x10 in Xamarin.Forms.Page.OnSizeAllocated
0x3 in Xamarin.Forms.VisualElement.SizeAllocated
0xD in Xamarin.Forms.Page.ForceLayout
0x1E in Xamarin.Forms.Page.set_ContainerArea
0x113 in Xamarin.Forms.Platform.iOS.NavigationRenderer.ViewDidLayoutSubviews
0xA in Envicase.Renderers.Views.Explore.ExploreMasterPageRenderer.ViewDidLayoutSubviews at c:\Users\Gyuwon\Documents\Projects\envicase\src\Envicase\Applications\Envicase.iOS\Renderers\Views\Explore\ExploreMasterPageRenderer.cs:29,-1
0xA6 in UIKit.UIApplication.UIApplicationMain
0xB in UIKit.UIApplication.Main at /Developer/MonoTouch/Source/monotouch/src/UIKit/UIApplication.cs:62,4
0x3B in UIKit.UIApplication.Main at /Developer/MonoTouch/Source/monotouch/src/UIKit/UIApplication.cs:46,4
0x78 in Envicase.IOSApp.Main at c:\Users\Gyuwon\Documents\Projects\envicase\src\Envicase\Applications\Envicase.iOS\IOSApp.cs:101,-1

원인

1.3.4.6332 버전에서 ScrollViewRenderer.SetElement() 메서드가 수정되었는데 새로 추가된 OnNativeControlUpdated 이벤트 핸들러가 등록됩니다. 이 코드는 현재 1.4.0.6336-pre1 버전까지 남아있습니다.

private void OnNativeControlUpdated(object sender, EventArgs eventArgs)
{
    this.ContentSize = this.Bounds.Size;
    this.UpdateContentSize();
}

OnNativeControlUpdated() 메서드가 호출될 때 ContentSize 속성이 변경되면서 스크롤됩니다.

NavigationPage.HasNavigationBarProperty 바인딩 속성이 true인 페에지에 포함되어 있을 때에만 발생하는 이유는 NavigationRenderer.ViewDidLayoutSubviews() 메서드에서 이 속성에 대한 분기처리를 하기 때문입니다.

public override void ViewDidLayoutSubviews()
{
    nfloat bottom;
    nfloat height;
    base.ViewDidLayoutSubviews();
    this.UpdateToolBarVisible();
    CGRect frame = this.NavigationBar.Frame;
    UIToolbar cGRect = this.secondaryToolbar;
    if (this.NavigationBarHidden || !NavigationPage.GetHasNavigationBar(this.Current))
    {
        bottom = 0;
    }
    else
    {
        bottom = frame.Bottom;
    }
    nfloat _nfloat = bottom;
    nfloat _nfloat1 = 0;
    nfloat width = this.View.Frame.Width;
    CGRect frame1 = cGRect.Frame;
    cGRect.Frame = new CGRect(_nfloat1, _nfloat, width, frame1.Height);
    double num = (double)((cGRect.Hidden ? _nfloat : cGRect.Frame.Bottom));
    Size size = (this.queuedSize.IsZero ? this.Element.Bounds.Size : this.queuedSize);
    NavigationPage element = (NavigationPage)this.Element;
    if (cGRect.Hidden)
    {
        height = 0;
    }
    else
    {
        height = cGRect.Frame.Height;
    }
    element.ContainerArea = new Rectangle(0, (double)height, size.Width, size.Height - num);
    if (!this.queuedSize.IsZero)
    {
        this.Element.Layout(new Rectangle(this.Element.X, this.Element.Y, this.queuedSize.Width, this.queuedSize.Height));
        this.queuedSize = Size.Zero;
    }
    this.loaded = true;
    UIView[] subviews = this.View.Subviews;
    for (int i = 0; i < (int)subviews.Length; i++)
    {
        UIView bounds = subviews[i];
        if (bounds != this.NavigationBar && bounds != this.secondaryToolbar)
        {
            bounds.Frame = this.View.Bounds;
        }
    }
}

해결책

UIKit.UIScrollView 클래스의 ContentSize 속성이 가상이라는 점을 이용할 수 있을 것으로 기대했지만 스와이프 내비게이션 처리가 쉽지않습니다. 그래서 리플렉션을 사용해 OnNativeControlUpdated() 이벤트 핸들러가 호출되지 않도록 도와주는 클래스를 작성합니다.

internal static class ScrollViewIssueResolver
{
    public static void Resolve(ScrollViewRenderer renderer)
    {
        if (renderer == null)
        {
            throw Error.That.ArgumentNull(() => renderer);
        }

        var trackerField = typeof(ScrollViewRenderer).GetField(
                   name: "tracker",
            bindingAttr: BindingFlags.NonPublic | BindingFlags.Instance);
        var tracker = (VisualElementTracker)trackerField.GetValue(renderer);
        var onNativeControlUpdatedMethod = typeof(ScrollViewRenderer).GetMethod(
                   name: "OnNativeControlUpdated",
            bindingAttr: BindingFlags.NonPublic | BindingFlags.Instance);

        var nativeControlUpdatedField = typeof(VisualElementTracker).GetField(
                   name: "NativeControlUpdated",
            bindingAttr: BindingFlags.NonPublic | BindingFlags.Instance);
        nativeControlUpdatedField.SetValue(
              obj: tracker,
            value: System.Delegate.RemoveAll(
                source: (EventHandler)nativeControlUpdatedField.GetValue(tracker),
                 value: onNativeControlUpdatedMethod.CreateDelegate(
                    delegateType: typeof(EventHandler),
                          target: renderer)));
    }
}

OnNativeControlUpdated() 이벤트 핸들러가 등록된 뒤 OnElementChanged() 메서드가 호출됩니다. 여기에서 ScrollViewIssueResolver.Resolve() 메서드를 호출하면 맨 위로 스트롤되는 현상을 막을 수 있습니다.

protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
    base.OnElementChanged(e);

    // Xamarin Forms 1.3.4.6332에서 발생하는 문제를 해결하기 위한 임시 수단입니다.
    ScrollViewIssueResolver.Resolve(this);
}

하지만 하나의 문제를 해결하기 위해 완성된 패키지의 코드를 수정하는 것이라 위험한 시도이기 때문에 꼭 필요한 곳에만 사용하고 파생되는 문제가 없는지 꼼꼼하게 테스트해야 합니다.

Advertisements

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중