読者です 読者をやめる 読者になる 読者になる

NSBlogger

意識高いブログ

UIScrollView.contentOffsetの謎

iOS swift

UIScrollView.contentOffset

UIKitに含まれるUIScrollViewのプロパティ「contentOffset」。

public class UIScrollView : UIView, NSCoding {
        public var contentOffset: CGPoint // default CGPointZero

contentOffsetはどれだけスクロールしたかを表します。型はCGPoint。

contentOffsetの値はまるめられる

もしかしたら常識なのかもしれないですが、最近デバッグしてて気付きました。結論は、contentOffsetの値はまるめられるということ。
contentOffsetは1.0ずつ更新されます。ただし、addSubViewされていると0.5ずつ更新されます。

検証

検証はiPhone 6s 実機で行いました。

let scrollView = UIScrollView.init(frame: CGRectMake(0, 0, 375, 100))
scrollView.contentSize = CGSizeMake(1125, 100)

//view.addSubview(scrollView)

for i in 1...100 {
    let n : CGFloat = 0.01 * CGFloat(i)
    let point = CGPointMake(n, n)
    scrollView.contentOffset = point
    print("n:\(formattedNumber(n)))-> point:\(formattedPoint(point)) -> contentOffset:\(formattedPoint(scrollView.contentOffset))")
}

formattedなんちゃらというメソッドはコンソール結果をみやすくするために作りました。

func formattedNumber(f:CGFloat) -> NSString {
    return NSString(format: "%0.2f", f)
}

func formattedPoint(p:CGPoint) -> NSString {
    return "(\(formattedNumber(p.x)),\(formattedNumber(p.y)))"
}

結果

n:0.01)-> point:(0.01,0.01) -> contentOffset:(0.00,0.00)
n:0.02)-> point:(0.02,0.02) -> contentOffset:(0.00,0.00)
n:0.03)-> point:(0.03,0.03) -> contentOffset:(0.00,0.00)
n:0.04)-> point:(0.04,0.04) -> contentOffset:(0.00,0.00)
n:0.05)-> point:(0.05,0.05) -> contentOffset:(0.00,0.00)
n:0.06)-> point:(0.06,0.06) -> contentOffset:(0.00,0.00)
n:0.07)-> point:(0.07,0.07) -> contentOffset:(0.00,0.00)
n:0.08)-> point:(0.08,0.08) -> contentOffset:(0.00,0.00)
n:0.09)-> point:(0.09,0.09) -> contentOffset:(0.00,0.00)
n:0.10)-> point:(0.10,0.10) -> contentOffset:(0.00,0.00)
n:0.11)-> point:(0.11,0.11) -> contentOffset:(0.00,0.00)
n:0.12)-> point:(0.12,0.12) -> contentOffset:(0.00,0.00)
n:0.13)-> point:(0.13,0.13) -> contentOffset:(0.00,0.00)
n:0.14)-> point:(0.14,0.14) -> contentOffset:(0.00,0.00)
n:0.15)-> point:(0.15,0.15) -> contentOffset:(0.00,0.00)
n:0.16)-> point:(0.16,0.16) -> contentOffset:(0.00,0.00)
n:0.17)-> point:(0.17,0.17) -> contentOffset:(0.00,0.00)
n:0.18)-> point:(0.18,0.18) -> contentOffset:(0.00,0.00)
n:0.19)-> point:(0.19,0.19) -> contentOffset:(0.00,0.00)
n:0.20)-> point:(0.20,0.20) -> contentOffset:(0.00,0.00)
n:0.21)-> point:(0.21,0.21) -> contentOffset:(0.00,0.00)
n:0.22)-> point:(0.22,0.22) -> contentOffset:(0.00,0.00)
n:0.23)-> point:(0.23,0.23) -> contentOffset:(0.00,0.00)
n:0.24)-> point:(0.24,0.24) -> contentOffset:(0.00,0.00)
n:0.25)-> point:(0.25,0.25) -> contentOffset:(0.00,0.00)
n:0.26)-> point:(0.26,0.26) -> contentOffset:(0.00,0.00)
n:0.27)-> point:(0.27,0.27) -> contentOffset:(0.00,0.00)
n:0.28)-> point:(0.28,0.28) -> contentOffset:(0.00,0.00)
n:0.29)-> point:(0.29,0.29) -> contentOffset:(0.00,0.00)
n:0.30)-> point:(0.30,0.30) -> contentOffset:(0.00,0.00)
n:0.31)-> point:(0.31,0.31) -> contentOffset:(0.00,0.00)
n:0.32)-> point:(0.32,0.32) -> contentOffset:(0.00,0.00)
n:0.33)-> point:(0.33,0.33) -> contentOffset:(0.00,0.00)
n:0.34)-> point:(0.34,0.34) -> contentOffset:(0.00,0.00)
n:0.35)-> point:(0.35,0.35) -> contentOffset:(0.00,0.00)
n:0.36)-> point:(0.36,0.36) -> contentOffset:(0.00,0.00)
n:0.37)-> point:(0.37,0.37) -> contentOffset:(0.00,0.00)
n:0.38)-> point:(0.38,0.38) -> contentOffset:(0.00,0.00)
n:0.39)-> point:(0.39,0.39) -> contentOffset:(0.00,0.00)
n:0.40)-> point:(0.40,0.40) -> contentOffset:(0.00,0.00)
n:0.41)-> point:(0.41,0.41) -> contentOffset:(0.00,0.00)
n:0.42)-> point:(0.42,0.42) -> contentOffset:(0.00,0.00)
n:0.43)-> point:(0.43,0.43) -> contentOffset:(0.00,0.00)
n:0.44)-> point:(0.44,0.44) -> contentOffset:(0.00,0.00)
n:0.45)-> point:(0.45,0.45) -> contentOffset:(0.00,0.00)
n:0.46)-> point:(0.46,0.46) -> contentOffset:(0.00,0.00)
n:0.47)-> point:(0.47,0.47) -> contentOffset:(0.00,0.00)
n:0.48)-> point:(0.48,0.48) -> contentOffset:(0.00,0.00)
n:0.49)-> point:(0.49,0.49) -> contentOffset:(0.00,0.00)
n:0.50)-> point:(0.50,0.50) -> contentOffset:(1.00,1.00)
n:0.51)-> point:(0.51,0.51) -> contentOffset:(1.00,1.00)
n:0.52)-> point:(0.52,0.52) -> contentOffset:(1.00,1.00)
n:0.53)-> point:(0.53,0.53) -> contentOffset:(1.00,1.00)
n:0.54)-> point:(0.54,0.54) -> contentOffset:(1.00,1.00)
n:0.55)-> point:(0.55,0.55) -> contentOffset:(1.00,1.00)
n:0.56)-> point:(0.56,0.56) -> contentOffset:(1.00,1.00)
n:0.57)-> point:(0.57,0.57) -> contentOffset:(1.00,1.00)
n:0.58)-> point:(0.58,0.58) -> contentOffset:(1.00,1.00)
n:0.59)-> point:(0.59,0.59) -> contentOffset:(1.00,1.00)
n:0.60)-> point:(0.60,0.60) -> contentOffset:(1.00,1.00)
n:0.61)-> point:(0.61,0.61) -> contentOffset:(1.00,1.00)
n:0.62)-> point:(0.62,0.62) -> contentOffset:(1.00,1.00)
n:0.63)-> point:(0.63,0.63) -> contentOffset:(1.00,1.00)
n:0.64)-> point:(0.64,0.64) -> contentOffset:(1.00,1.00)
n:0.65)-> point:(0.65,0.65) -> contentOffset:(1.00,1.00)
n:0.66)-> point:(0.66,0.66) -> contentOffset:(1.00,1.00)
n:0.67)-> point:(0.67,0.67) -> contentOffset:(1.00,1.00)
n:0.68)-> point:(0.68,0.68) -> contentOffset:(1.00,1.00)
n:0.69)-> point:(0.69,0.69) -> contentOffset:(1.00,1.00)
n:0.70)-> point:(0.70,0.70) -> contentOffset:(1.00,1.00)
n:0.71)-> point:(0.71,0.71) -> contentOffset:(1.00,1.00)
n:0.72)-> point:(0.72,0.72) -> contentOffset:(1.00,1.00)
n:0.73)-> point:(0.73,0.73) -> contentOffset:(1.00,1.00)
n:0.74)-> point:(0.74,0.74) -> contentOffset:(1.00,1.00)
n:0.75)-> point:(0.75,0.75) -> contentOffset:(1.00,1.00)
n:0.76)-> point:(0.76,0.76) -> contentOffset:(1.00,1.00)
n:0.77)-> point:(0.77,0.77) -> contentOffset:(1.00,1.00)
n:0.78)-> point:(0.78,0.78) -> contentOffset:(1.00,1.00)
n:0.79)-> point:(0.79,0.79) -> contentOffset:(1.00,1.00)
n:0.80)-> point:(0.80,0.80) -> contentOffset:(1.00,1.00)
n:0.81)-> point:(0.81,0.81) -> contentOffset:(1.00,1.00)
n:0.82)-> point:(0.82,0.82) -> contentOffset:(1.00,1.00)
n:0.83)-> point:(0.83,0.83) -> contentOffset:(1.00,1.00)
n:0.84)-> point:(0.84,0.84) -> contentOffset:(1.00,1.00)
n:0.85)-> point:(0.85,0.85) -> contentOffset:(1.00,1.00)
n:0.86)-> point:(0.86,0.86) -> contentOffset:(1.00,1.00)
n:0.87)-> point:(0.87,0.87) -> contentOffset:(1.00,1.00)
n:0.88)-> point:(0.88,0.88) -> contentOffset:(1.00,1.00)
n:0.89)-> point:(0.89,0.89) -> contentOffset:(1.00,1.00)
n:0.90)-> point:(0.90,0.90) -> contentOffset:(1.00,1.00)
n:0.91)-> point:(0.91,0.91) -> contentOffset:(1.00,1.00)
n:0.92)-> point:(0.92,0.92) -> contentOffset:(1.00,1.00)
n:0.93)-> point:(0.93,0.93) -> contentOffset:(1.00,1.00)
n:0.94)-> point:(0.94,0.94) -> contentOffset:(1.00,1.00)
n:0.95)-> point:(0.95,0.95) -> contentOffset:(1.00,1.00)
n:0.96)-> point:(0.96,0.96) -> contentOffset:(1.00,1.00)
n:0.97)-> point:(0.97,0.97) -> contentOffset:(1.00,1.00)
n:0.98)-> point:(0.98,0.98) -> contentOffset:(1.00,1.00)
n:0.99)-> point:(0.99,0.99) -> contentOffset:(1.00,1.00)
n:1.00)-> point:(1.00,1.00) -> contentOffset:(1.00,1.00)

0.5を境に値が変わっています。0.00〜0.50, 0.50〜1.00, 1.00〜1.50という区切りで値が変わります。contentOffsetは1.0ずつ変動します。

結果(addSubviewする)

色々ためしていたら、あることに気づきました。UIScrollViewが他のviewにaddSubviewされていると結果が異なります。上記のコメントアウトをはずした結果がこちら。

n:0.01)-> point:(0.01,0.01) -> contentOffset:(0.00,0.00)
n:0.02)-> point:(0.02,0.02) -> contentOffset:(0.00,0.00)
n:0.03)-> point:(0.03,0.03) -> contentOffset:(0.00,0.00)
n:0.04)-> point:(0.04,0.04) -> contentOffset:(0.00,0.00)
n:0.05)-> point:(0.05,0.05) -> contentOffset:(0.00,0.00)
n:0.06)-> point:(0.06,0.06) -> contentOffset:(0.00,0.00)
n:0.07)-> point:(0.07,0.07) -> contentOffset:(0.00,0.00)
n:0.08)-> point:(0.08,0.08) -> contentOffset:(0.00,0.00)
n:0.09)-> point:(0.09,0.09) -> contentOffset:(0.00,0.00)
n:0.10)-> point:(0.10,0.10) -> contentOffset:(0.00,0.00)
n:0.11)-> point:(0.11,0.11) -> contentOffset:(0.00,0.00)
n:0.12)-> point:(0.12,0.12) -> contentOffset:(0.00,0.00)
n:0.13)-> point:(0.13,0.13) -> contentOffset:(0.00,0.00)
n:0.14)-> point:(0.14,0.14) -> contentOffset:(0.00,0.00)
n:0.15)-> point:(0.15,0.15) -> contentOffset:(0.00,0.00)
n:0.16)-> point:(0.16,0.16) -> contentOffset:(0.00,0.00)
n:0.17)-> point:(0.17,0.17) -> contentOffset:(0.00,0.00)
n:0.18)-> point:(0.18,0.18) -> contentOffset:(0.00,0.00)
n:0.19)-> point:(0.19,0.19) -> contentOffset:(0.00,0.00)
n:0.20)-> point:(0.20,0.20) -> contentOffset:(0.00,0.00)
n:0.21)-> point:(0.21,0.21) -> contentOffset:(0.00,0.00)
n:0.22)-> point:(0.22,0.22) -> contentOffset:(0.00,0.00)
n:0.23)-> point:(0.23,0.23) -> contentOffset:(0.00,0.00)
n:0.24)-> point:(0.24,0.24) -> contentOffset:(0.00,0.00)
n:0.25)-> point:(0.25,0.25) -> contentOffset:(0.50,0.50)
n:0.26)-> point:(0.26,0.26) -> contentOffset:(0.50,0.50)
n:0.27)-> point:(0.27,0.27) -> contentOffset:(0.50,0.50)
n:0.28)-> point:(0.28,0.28) -> contentOffset:(0.50,0.50)
n:0.29)-> point:(0.29,0.29) -> contentOffset:(0.50,0.50)
n:0.30)-> point:(0.30,0.30) -> contentOffset:(0.50,0.50)
n:0.31)-> point:(0.31,0.31) -> contentOffset:(0.50,0.50)
n:0.32)-> point:(0.32,0.32) -> contentOffset:(0.50,0.50)
n:0.33)-> point:(0.33,0.33) -> contentOffset:(0.50,0.50)
n:0.34)-> point:(0.34,0.34) -> contentOffset:(0.50,0.50)
n:0.35)-> point:(0.35,0.35) -> contentOffset:(0.50,0.50)
n:0.36)-> point:(0.36,0.36) -> contentOffset:(0.50,0.50)
n:0.37)-> point:(0.37,0.37) -> contentOffset:(0.50,0.50)
n:0.38)-> point:(0.38,0.38) -> contentOffset:(0.50,0.50)
n:0.39)-> point:(0.39,0.39) -> contentOffset:(0.50,0.50)
n:0.40)-> point:(0.40,0.40) -> contentOffset:(0.50,0.50)
n:0.41)-> point:(0.41,0.41) -> contentOffset:(0.50,0.50)
n:0.42)-> point:(0.42,0.42) -> contentOffset:(0.50,0.50)
n:0.43)-> point:(0.43,0.43) -> contentOffset:(0.50,0.50)
n:0.44)-> point:(0.44,0.44) -> contentOffset:(0.50,0.50)
n:0.45)-> point:(0.45,0.45) -> contentOffset:(0.50,0.50)
n:0.46)-> point:(0.46,0.46) -> contentOffset:(0.50,0.50)
n:0.47)-> point:(0.47,0.47) -> contentOffset:(0.50,0.50)
n:0.48)-> point:(0.48,0.48) -> contentOffset:(0.50,0.50)
n:0.49)-> point:(0.49,0.49) -> contentOffset:(0.50,0.50)
n:0.50)-> point:(0.50,0.50) -> contentOffset:(0.50,0.50)
n:0.51)-> point:(0.51,0.51) -> contentOffset:(0.50,0.50)
n:0.52)-> point:(0.52,0.52) -> contentOffset:(0.50,0.50)
n:0.53)-> point:(0.53,0.53) -> contentOffset:(0.50,0.50)
n:0.54)-> point:(0.54,0.54) -> contentOffset:(0.50,0.50)
n:0.55)-> point:(0.55,0.55) -> contentOffset:(0.50,0.50)
n:0.56)-> point:(0.56,0.56) -> contentOffset:(0.50,0.50)
n:0.57)-> point:(0.57,0.57) -> contentOffset:(0.50,0.50)
n:0.58)-> point:(0.58,0.58) -> contentOffset:(0.50,0.50)
n:0.59)-> point:(0.59,0.59) -> contentOffset:(0.50,0.50)
n:0.60)-> point:(0.60,0.60) -> contentOffset:(0.50,0.50)
n:0.61)-> point:(0.61,0.61) -> contentOffset:(0.50,0.50)
n:0.62)-> point:(0.62,0.62) -> contentOffset:(0.50,0.50)
n:0.63)-> point:(0.63,0.63) -> contentOffset:(0.50,0.50)
n:0.64)-> point:(0.64,0.64) -> contentOffset:(0.50,0.50)
n:0.65)-> point:(0.65,0.65) -> contentOffset:(0.50,0.50)
n:0.66)-> point:(0.66,0.66) -> contentOffset:(0.50,0.50)
n:0.67)-> point:(0.67,0.67) -> contentOffset:(0.50,0.50)
n:0.68)-> point:(0.68,0.68) -> contentOffset:(0.50,0.50)
n:0.69)-> point:(0.69,0.69) -> contentOffset:(0.50,0.50)
n:0.70)-> point:(0.70,0.70) -> contentOffset:(0.50,0.50)
n:0.71)-> point:(0.71,0.71) -> contentOffset:(0.50,0.50)
n:0.72)-> point:(0.72,0.72) -> contentOffset:(0.50,0.50)
n:0.73)-> point:(0.73,0.73) -> contentOffset:(0.50,0.50)
n:0.74)-> point:(0.74,0.74) -> contentOffset:(0.50,0.50)
n:0.75)-> point:(0.75,0.75) -> contentOffset:(1.00,1.00)
n:0.76)-> point:(0.76,0.76) -> contentOffset:(1.00,1.00)
n:0.77)-> point:(0.77,0.77) -> contentOffset:(1.00,1.00)
n:0.78)-> point:(0.78,0.78) -> contentOffset:(1.00,1.00)
n:0.79)-> point:(0.79,0.79) -> contentOffset:(1.00,1.00)
n:0.80)-> point:(0.80,0.80) -> contentOffset:(1.00,1.00)
n:0.81)-> point:(0.81,0.81) -> contentOffset:(1.00,1.00)
n:0.82)-> point:(0.82,0.82) -> contentOffset:(1.00,1.00)
n:0.83)-> point:(0.83,0.83) -> contentOffset:(1.00,1.00)
n:0.84)-> point:(0.84,0.84) -> contentOffset:(1.00,1.00)
n:0.85)-> point:(0.85,0.85) -> contentOffset:(1.00,1.00)
n:0.86)-> point:(0.86,0.86) -> contentOffset:(1.00,1.00)
n:0.87)-> point:(0.87,0.87) -> contentOffset:(1.00,1.00)
n:0.88)-> point:(0.88,0.88) -> contentOffset:(1.00,1.00)
n:0.89)-> point:(0.89,0.89) -> contentOffset:(1.00,1.00)
n:0.90)-> point:(0.90,0.90) -> contentOffset:(1.00,1.00)
n:0.91)-> point:(0.91,0.91) -> contentOffset:(1.00,1.00)
n:0.92)-> point:(0.92,0.92) -> contentOffset:(1.00,1.00)
n:0.93)-> point:(0.93,0.93) -> contentOffset:(1.00,1.00)
n:0.94)-> point:(0.94,0.94) -> contentOffset:(1.00,1.00)
n:0.95)-> point:(0.95,0.95) -> contentOffset:(1.00,1.00)
n:0.96)-> point:(0.96,0.96) -> contentOffset:(1.00,1.00)
n:0.97)-> point:(0.97,0.97) -> contentOffset:(1.00,1.00)
n:0.98)-> point:(0.98,0.98) -> contentOffset:(1.00,1.00)
n:0.99)-> point:(0.99,0.99) -> contentOffset:(1.00,1.00)
n:1.00)-> point:(1.00,1.00) -> contentOffset:(1.00,1.00)

0.25と0.75を境に値が変わっています。-0.25〜0.25, 0.25〜0.75, 0.75〜1.25という区切りで値が変わります。addSubviewするとcontentOffsetが0.5ずつ変動しました。

contentSizeやframeは?

検証のコードは省略しますが、contentSizeおよびframeの値がどのように変化するか調べてみたところ、つねに代入した値がそのまま反映されていました。上記の例だと0.01ずつ変化します。つまり、contentOffsetだけ値がまるめられていました。

なぜ?

なぜこのようなことが起きるのでしょうか。よく分かっていません。
UIScrollViewDelegateにscrollViewDidScroll(_:)があります。UIScrollViewをスクロールさせてcontentOffsetが変化したときに毎度呼ばれますね。ゆっくりスクロールしてみるとわかりますが、0.5動いたときに初めて呼ばれます。もしかすると、もっと細かく(たとえば0.01ずつ)変動すると、ほんのちょっと動かしただけで呼びだされてiOS的にあまりよろしくない処理になるのかなと思いました。負荷がかかったり、厳密に判断しすぎてユーザの思った動作にそぐわない動きをするなど…。もしご存知でしたら教えて下さい。