最近抽空看了些 macOS 開發的資料。(自嘲下:iOS 開發都不是很會,就開始搞 macOS 開發。。)
一開始覺得 macOS 和 iOS 估計差不多。但是呢,習慣 UIKit,再去碰 Appkit 這個古老的框架。只能說兩者真不是一碼事。。。
官方有份 NSScrollView 的教程。寫的挺詳細。可以看下。
Xcode 里面的文檔沒有很多的解釋。有些連默認值都不知道有沒有。使用 UIKit 時,遇到問題,跳到文檔里面,很多情況下會有相應解釋等。但是 AppKit 的話,大部分都沒有多少解釋。估計又是一個歷史包袱吧~
那么如何去創建一個 NSScrollView?
let scrollView = NSScrollView()
let scrollView = NSScrollView(frame: NSRect())
這個和 UIScrollView 幾乎一致。
UIKit 中,我們可以通過設置 UIScrollView 的 contentSize 來進行可滑動的操作。但是呢,在 NSScrollView 里,並沒有 contentSize 供我們設置,取而代之的是 documentView。當我們要使用一個 NSScrollView 的時候,需要把 conentView 指向 scrollView 的 documentView。
let contentView = NSView(frame: NSRect(x: 0, y: 0, width: 1000, height: 1000))
contentView.backgroundColor = .yellow
bgScrollView.documentView = contentView
在官方的教程中,解釋的很清楚。

NSScrollView 是由 NSScrollerNSClipViewContentViewNSRulerView 構成的。這個可以選擇用 IB 構建一個 NSScrollView 來查看,比較直觀。

Clip View ,是一個 NSClipView。也就是 scrollView 的一個 contentView
的屬性。官方的說法,負責剪切 documentView 的內容等。從 IB 的 圖看到的層級,猜測 documentView 有可能就是這個 NSClipView 層級下的 view。
NSScrollView 給人的感覺,更像一個迷你窗口,然后通過滾動 documentView 來使的內容出現在窗口里面。
在使用上,一些屬性配置可以見以下代碼。
let scrollView = NSScrollView()
// scrollerStyle。overlay / legacy。 overlay 的效果,則是 scroller 背景透明,而 legacy 則是 獨立出 scroller 的區域,看起來比較丑~~個人覺得?
scrollView.scrollerStyle = .overlay
// 滾動條的顯示。用 IB 創建 scrollview 時,以下兩個參數均為 true。但是 code 創建 scrollview 時,以下兩個參數默認均為 false。很奇怪的設計。
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = true
// 滾動條的樣式:light、 dark、default。default 的話,其實就是 dark
scrollView.scrollerKnobStyle = .dark
// bounce 的效果。elasticity 是彈性的含義。automatic\allowed\none。
scrollView.horizontalScrollElasticity = .automatic
scrollView.verticalScrollElasticity = .automatic
由於 macOS 的坐標起點是在屏幕左下角。因此,在設置好 documentView 后,最好讓 scrollView 滾到最上方的位置。為了方便,直接用 extension 增加了一個 scrollToTop 的方法。
extension NSScrollView {
/// Scroll to the ducument view top.
public
func scrollToTop() {
if let documentView = self.documentView {
if documentView.isFlipped {
documentView.scroll(.zero)
} else {
let maxHeight = max(bounds.height, documentView.bounds.height)
documentView.scroll(NSPoint(x: 0, y: maxHeight))
}
}
}
}
// 滾動到最上方
scrollView.scrollToTop()
-(void)scrollPoint:(NSPoint)aPoint
- (BOOL)scrollRectToVisible:(NSRect)aRect
NSView的enclosingScrollView屬性可以獲得視圖的滾動條,如果視圖沒有滾動條則enclosingScrollView為nil。
滾動到視圖頂部的代碼
func scrollToPoint() {
let sframe = CGRect(x: 0, y: 0, width: 200, height: 200)
let scrollView = NSScrollView(frame: sframe)
let image = NSImage(named: "img.png")
let imageViewFrame = CGRect(x: 0, y: 0, width: (image?.size.width)!, height: (image?.size.height)!)
let imageView = NSImageView(frame: imageViewFrame)
imageView.image = image
scrollView.hasVerticalRuler = true
scrollView.hasHorizontalRuler = true
scrollView.documentView = imageView
self.view.addSubview(imageView)
//滾動到頂部位置var newScrollOrigin: NSPoint
var newScrollOrign: NSPoint
let contentView: NSClipView = scrollView.contentView
if self.view.isFlipped {
newScrollOrign = NSPoint(x: 0.0, y: 0.0)
}else{
newScrollOrign = NSPoint(x: 0, y: imageView.frame.size.height - contentView.frame.size.height)
}
contentView.scroll(to: newScrollOrign)
}
其中,isFlipped 是坐標系翻轉。是 NSView 的一個屬性。
當然,NSScrollView 並非只有這么簡簡單單幾行代碼。還有一些設置。比如 scrollerInsets 是設置 NSScroller 的邊距等。另外,還有一些通知,可以進行一些事件的監聽。
最后上個demo。
滾動條的顯示控制
滾動條的 has VerticalScroller和 hasHorizontalScroller分別用來控制是否顯示縱向和橫向的滾動條。如果設置它們為 false,只是不顯示出來,並不是禁止滾動的行為。但是大多數情況下上述兩個方法並不能真正實現滾動條的完全不顯示,要做到完全不顯示滾動條,需要重寫滾動條類的tile方法,通過設置水平和垂直方向滾動條的size中的 width和 height為0來實現。
下面的代碼定義了滾動條的子類 NoScrollerScrollView,重寫了它的tile方法,實現了完全隱藏滾動條。
如果要禁止一個方向的滾動,需要子類化 NSScrollview,重載它的 scrollwheel方法,判斷
y軸方向的偏移量滿足一定條件返回即可
class NoScrollerScrollView: NSScrollView {
//重載tile 隱藏滾動條
override func tile() {
super.tile()
var hFrame = self.horizontalScroller?.frame
hFrame?.size.height = 0
if let hframe = hFrame {
self.horizontalScroller?.frame = hframe
}
var vFrame = self.verticalScroller?.frame
vFrame?.size.width = 0
if let vframe = vFrame {
self.verticalScroller?.frame = vframe
}
}
//禁止一個方向上的滾動重載scrollView
override func scrollWheel(with event: NSEvent) {
let f = abs(event.deltaY)
if event.deltaX == 0.0 && f >= 0.01 {
return
}else if event.deltaX == 0 && f == 0.0 {
return
}
else {
super.scrollWheel(with: event)
}
}
}
