SwiftUI中.frame修飾器的使用

在學(xué)習(xí) SwiftUI 的過程中拳缠,首先會學(xué)到的 modifiers 可能就是 .frame逻淌,關(guān)·modifiers這里有必要提一下 祭埂,其實SwiftU中的modifies并不是改變View上的某個屬性而是用一個帶有相關(guān)屬性的新View來包裝原有的View趴捅,當(dāng)然.frame也不例外套硼,具體的modifiers的使用可以見我的另外一篇文章卡辰,本篇文章主要弄清楚.frameSwiftUI布局體系中到底扮演著什么重要的角色。

基本View

SwiftUI的布局原則其實很簡單:對于層級中的View邪意,SwiftUI都會提供一個建議尺寸 九妈,然后View將自己布局在這個建議尺寸的可用空間內(nèi),并報告自己的實際尺寸雾鬼,默認(rèn)情況下系統(tǒng)將View置于可用空間的中心萌朱。但是View的實際尺寸和收到的建議尺寸是會有出入的,比如實際尺寸比建議尺寸大該如何顯示策菜?實際尺寸比建議尺寸小呢晶疼?有多的空間時該如何分配給各個View呢?這些都和SwiftUI中的基本View有關(guān)又憨,下面我們從基本的View入手翠霍。

stack

Stack是盡可能的占據(jù)更多的空間在滿足自己的內(nèi)容,可不會委屈它們蠢莺。

Text

Text的行為和UIKit中的差不多寒匙,這類View的特點是及其''老實本分'',根據(jù)自身內(nèi)容來占據(jù)應(yīng)有的尺寸躏将,當(dāng)空間不夠時像Text就寧愿改變自己也不會超出父View當(dāng)初提議的尺寸蒋情,比如寬度不夠時則截斷文本顯示,高度不夠時就顯示單行耸携,他們盡可能的尊重提議的尺寸。

Text("hello there, this is a long line that won't fit parent's size.")
     .border(Color.blue)
     .frame(width: 200, height:30)
     .border(Color.green)
     .font(.title)
     .padding(0)

由于.frame包裝后的ViewText提議的尺寸是寬度為200高度為30辕翰,但是Text的實際寬度是大于提議尺寸的200的夺衍,為了"尊重"提議的尺寸,Text截斷自身顯示喜命,當(dāng)然也是可以忽略提議尺寸的沟沙,后面會解釋到。

Image

默認(rèn)情況下Image的尺寸是固定的壁榕,其會忽略掉布局系統(tǒng)建議的尺寸而總是返回圖片的尺寸矛紫。要讓一個圖片 view尺寸可變,或者說牌里,想讓它能接受建議尺寸颊咬,并將圖片適配顯示在這個空間里务甥,我們可以在它上面調(diào)用 .resizable,默認(rèn)情況下喳篇,這會拉伸圖片敞临,讓它填滿整個建議尺寸的空間 (我們也可以設(shè)置成以瓷磚平鋪的方式,或者只拉伸圖片的某個部分來進(jìn)行填充)麸澜,因為大部分的圖片應(yīng)該是需要以固定的高寬比展示的挺尿,所以 .aspectRatio 經(jīng)常被直接搭配在 .resizable 后面組合使用。
.aspectRatio 修飾器會去獲取建議尺寸炊邦,并且基于給定的高寬比编矾,創(chuàng)建一個能最大限度填滿建議尺寸的新的尺寸值。接下來馁害,它會將這個尺寸建議給大小可變的圖像 (resizable 的圖像會填滿整個建議尺寸)窄俏,并將該尺寸返回給上層View。我們可以選擇適配或者填充這個建議尺寸蜗细,我們也可以決定是要指定一個高寬比裆操,還是將高寬比留給子View去做決定。

let image = Image(systemName: "ellipsis")
HStack {
   image
   image.resizable()
   image.resizable().aspectRatio(contentMode: .fit)
}

Path

Path類型代表了一組 2D的繪制指令 (和Cocoa 中的 CGPath 類似)炉媒,它總會將建議的尺寸作為實際尺寸返回踪区,如果所建議的某個方向的值為 nil,那么它返回默認(rèn)值10吊骤。

Path { p in
    p.move(to: CGPoint(x: 50, y: 0))
    p.addLines([
      CGPoint(x: 100, y: 75),
      CGPoint(x: 0, y: 75),
      CGPoint(x: 50, y: 0)
  ])
}

可能上面的話你不太理解缎岗,那么我在寫的詳細(xì)點,下面的這段代碼你能想到為什么是這個效果嗎白粉?

 var body: some View {
    VStack(spacing:0) {
           Text("hello there, this is a long line that won't fit parent's size.")
           .frame(width: 200, height:30)
           .border(Color.green)
           .font(.title)
           .padding(0)
           Path { p in
                p.move(to: CGPoint(x: 50, y: 0))
                p.addLines([
                  CGPoint(x: 100, y: 75),
                  CGPoint(x: 0, y: 75),
                  CGPoint(x: 50, y: 0)
              ])
            }
    }
     .border(Color.red)
 }

布局流程解析:

  • 首先VStack會將整個屏幕尺寸建議給它的子Views传泊,這里就是TextPath
  • Text在顯示完自己的內(nèi)容后上報自己所需要的尺寸鸭巴,Path就比較特別眷细,上面我們已經(jīng)說到它總會將建議的尺寸作為實際尺寸返回,所以這里它會返回建議尺寸鹃祖,那么建議尺寸是什么溪椎?其實就是當(dāng)初VStack建議的尺寸減去Text所用的尺寸后的尺寸,因為VStack是盡可能占用更多的空間的恬口,所以VStack收到了TextPath上報的尺寸后就確定了自己的尺寸校读,上圖中紅色邊框顯示的區(qū)域。

如果Path所建議的某個方向的值為nil祖能,那么它返回的默認(rèn)值為10歉秫,采用.fixedSize可以把建議尺寸設(shè)置為nil

 var body: some View {
    VStack(spacing:0) {
           Text("hello there, this is a long line that won't fit parent's size.")
           .frame(width: 200, height:30)
           .border(Color.green)
           .font(.title)
           .padding(0)
           Path { p in
                p.move(to: CGPoint(x: 50, y: 0))
                p.addLines([
                  CGPoint(x: 100, y: 75),
                  CGPoint(x: 0, y: 75),
                  CGPoint(x: 50, y: 0)
              ])
            }
              .fixedSize(horizontal: false, vertical: true)
    }
     .border(Color.red)
 }

上面的代碼只是利用fixedsize忽略了Path豎直方向的建議尺寸,那么Path在此方向的上報尺寸則會忽略父View的建議尺寸和直接返回10养铸,所以VStack收到的Path的高度尺寸為10雁芙,所以紅色邊框的區(qū)域在高度方向會如下所示轧膘,Path繪制在了VStack的外面。

Shape

Path一樣 Shape也總會將建議的尺寸進(jìn)行返回却特,在某個方向上建議尺寸為nil時返回默認(rèn)值10扶供,Shape會盡可能的將自身繪制在建議尺寸甚至填滿建議尺寸,像是 Rectangle裂明、Circle椿浓、EllipseCapsule 這些內(nèi)建的形狀,會將它們自身繪制在建議尺寸中闽晦。那些沒有高寬比約束的形狀扳碍,像是 Rectangle,會選擇填滿整個可用的空間仙蛉,在布局過程中笋敞,Shape 會接收到 path(in:) 的調(diào)用,其中的 rect 參數(shù)所包含的尺寸正是建議尺寸荠瘪。

struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
    return Path { p in
          p.move(to: CGPoint(x: rect.midX, y: rect.minY))
          p.addLines([
          CGPoint(x: rect.maxX, y: rect.maxY),
          CGPoint(x: rect.minX, y: rect.maxY),
          CGPoint(x: rect.midX, y: rect.minY)
      ])
     }
   }
}

Frame的使用

方式一

在使用Frame的過程中很容易以為Frame設(shè)置的多大夯巷,View就會占據(jù)多少空間,其實Frame改變的只是建議尺寸哀墓,具體View的實際尺寸是要看View怎么來繪制自己的趁餐,上面我們已經(jīng)介紹了基礎(chǔ)的View,可以發(fā)現(xiàn)基礎(chǔ)View在收到建議尺寸時會有不同的表現(xiàn)形式篮绰,有的是尊重建議尺寸后雷,有的則填充建議尺寸,還有的甚至可以在建議尺寸外面繪制自己吠各。

Frame一共有二個初始化的方法:

  • func frame(width: CGFloat? = nil, height: CGFloat? = nil, alignment: Alignment = .center)
    
  • func frame(minWidth: CGFloat? = nil, idealWidth: CGFloat? = nil, maxWidth: CGFloat? = nil, minHeight: CGFloat? = nil, idealHeight: CGFloat? = nil, maxHeight: CGFloat? = nil, alignment: Alignment = .center)
    

在使用時參數(shù)是可變的臀突,可以只設(shè)置width或者height,也可以同時設(shè)置贾漏,其中關(guān)于aligment的內(nèi)容請看我的另外一篇文章候学。

struct ExampleView: View {
    @State private var width: CGFloat = 50
    
    var body: some View {
        VStack {
            SubView()
                .frame(width: self.width, height: 120)
                .border(Color.blue, width: 2)
            
            Text("Offered Width \(Int(width))")
            Slider(value: $width, in: 0...200, step: 1)
        }
    }
}

struct SubView: View {
    var body: some View {
        GeometryReader { proxy in
            Rectangle()
                .fill(Color.yellow.opacity(0.7))
                .frame(width: max(proxy.size.width, 120), height: max(proxy.size.height, 120))
        }
    }
}

代碼解讀

  • 首先VStack會將整個屏幕尺寸建議給它的子View,其中包括SubView纵散,TextSlider盒齿,當(dāng)然這里涉及到分配原則,比如有多的空間和空間不足時該如何分配的問題困食,這里我們先不談(后面會出文章專門聊這個)。
  • SubView經(jīng)過.frame修飾器修飾過后收到的建議寬度尺寸為width變量翎承,當(dāng)我們滑動Slider時會改變width變量的值硕盹。
  • 當(dāng)width變量的值小于120時,在SubView內(nèi)部利用GeometryReader讀取了這個width變量的建議寬度叨咖,由于采用max取的是最大值瘩例,所以SubView內(nèi)部的Rectangle會收到建議尺寸的寬度是120啊胶。
  • 上面我們已經(jīng)講過,對于Shape來說垛贤,Rectangle會在指定了建議尺寸時會填滿建議尺寸的焰坪,所以就算當(dāng)width變量的值小于120,由于Rectangle填滿了寬度120的尺寸聘惦,并將當(dāng)初建議的寬度120尺寸直接返回了某饰,所以SubView的寬度也為120了。
  • 當(dāng)width變量大于120時善绎,也很好理解黔漂,Rectangle依然填滿建議尺寸,所以SubView會跟著變寬禀酱。

上面的整個布局流程是一層層由外到內(nèi)炬守,然后由內(nèi)到外確定的,這種思想和UIKit是有很大的不同的剂跟,只有清理的知道SwiftUI內(nèi)部的布局流程减途,才能不會對有些實際出來的布局效果和自己所想有所出入時感到驚訝,上面的例子請好好體會曹洽,因為接下來會進(jìn)階到更難的鳍置。

方式二

上面的例子使用的是Frame的初始化方法1,對于初始化方法2則是指定最小衣洁,理想和最大尺寸墓捻,其中我們在傳值時minimumidealmaximum必須按照升序的方式傳入坊夫,不然會報錯砖第,我們可以將任意參數(shù)留空,這樣它們會使用默認(rèn)的nil值环凿,某個方向上的最小值和最大值將作為建議尺寸和返回尺寸的鉗位梧兼,舉例來說,當(dāng)我們對最大寬度進(jìn)行了配置時智听,.frame 修飾器會去檢查被建議的寬度羽杰,如果這個被建議的寬度超過了設(shè)置的最大寬度,那么它只會將最大寬度建議給它的子 view到推。類似地考赛,如果子view返回了一個比最大寬度更大的寬度,那么這個結(jié)果也將被鉗至最大寬度值莉测,當(dāng)在某個方向設(shè)置了fixedsize時颜骤,此時會采用ideal尺寸進(jìn)行布局。

struct ExampleView: View {
    @State private var width: CGFloat = 150
    @State private var fixedSize: Bool = true
    
    var body: some View {
        GeometryReader { proxy in
            
            VStack {
                Spacer()
                
                VStack {
                    LittleSquares(total: 7)
                        .border(Color.green)
                        .fixedSize(horizontal: self.fixedSize, vertical: false)
                }
                .frame(width: self.width)
                .border(Color.primary)
                .background(MyGradient())
                
                Spacer()
                
                Form {
                    Slider(value: self.$width, in: 0...proxy.size.width)
                    Toggle(isOn: self.$fixedSize) { Text("Fixed Width") }
                }
            }
        }.padding(.top, 140)
    }
}

struct LittleSquares: View {
    let sqSize: CGFloat = 20
    let total: Int
    
    var body: some View {
        GeometryReader { proxy in
            HStack(spacing: 5) {
                ForEach(0..<self.maxSquares(proxy), id: \.self) { _ in
                    RoundedRectangle(cornerRadius: 5).frame(width: self.sqSize, height: self.sqSize)
                        .foregroundColor(self.allFit(proxy) ? .green : .red)
                }
            }
              .border(Color.orange).frame(height:proxy.size.height)
        }.frame(idealWidth: (5 + self.sqSize) * CGFloat(self.total), maxWidth: (5 + self.sqSize) * CGFloat(self.total))
    }

    func maxSquares(_ proxy: GeometryProxy) -> Int {
        return min(Int(proxy.size.width / (sqSize + 5)), total)
    }
    
    func allFit(_ proxy: GeometryProxy) -> Bool {
        return maxSquares(proxy) == total
    }
}

struct MyGradient: View {
    var body: some View {
        LinearGradient(gradient: Gradient(colors: [Color.red.opacity(0.1), Color.green.opacity(0.1)]), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1))
    }
}

上面這個例子是一個綜合的例子捣卤,模擬的是類似Text的效果忍抽,當(dāng)空間足夠時則顯示7個小方格八孝,當(dāng)空間不夠時小方格盡可能的顯示,并將顏色變成紅色鸠项,下面是效果圖干跛,建議再看代碼截圖前,自己嘗試?yán)硐律鲜龃a的布局流程祟绊。

代碼解讀

  • 當(dāng)Toggle沒有打開時楼入,fixedSize這個變量為false,裝著LittleSquares的VStack容器通過.frameLittleSquares傳遞了指為變量width的寬度建議尺寸久免。
  • 由于fixedSizefalse浅辙, 所以不會啟動ideal尺寸參與布局,在LittleSquares的內(nèi)部通過GeometryReader拿到的proxy就是上面的變量width的值阎姥,同時把這個值傳遞給了maxSquares方法來計算能最大顯示方格的數(shù)量记舆,封頂數(shù)量是7個,allFit方法則簡單根據(jù)顯示的數(shù)量是否為7個改變了方格的顏色呼巴。
  • 注意到GeometryReader設(shè)置了frame泽腮,當(dāng)它收到的建議尺寸寬度為nil時會將對內(nèi)部HStack的建議尺寸用idealWidth代替,此時由于fixedSizefalse衣赶,所以不會使用idealWidth的值建議給內(nèi)部的HStack诊赊,而是采用maxWidth的值,當(dāng)它收到的建議尺寸的寬度超過了這個maxWidth的時候它將建議尺寸的寬度鉗至maxWidth的值傳遞給內(nèi)部的HStack府瞄,如果小于maxWidth凯楔,則直接把收到的建議寬度尺寸傳遞給內(nèi)部的HStack灸叼,同時在收到內(nèi)部HStack上報返回的尺寸時酬核,也會利用這個規(guī)則赦肃,如果超過了maxWidth則也會被鉗至maxWidth這個值。
  • 所以當(dāng)滑動Slider使紅色的矩形框變大時货邓,矩形也只會最多顯示7個秆撮,但是由于沒有設(shè)置minWidth,所以當(dāng)紅色的矩形框變小時則不足以顯示7個换况,會根據(jù)收到的建議尺寸空間盡量的顯示更多的方格职辨。
  • 當(dāng)Toggle沒有打開時,fixedSize這個變量為true戈二,GeometryReaderidealWidth傳遞給了內(nèi)部的HStack舒裤,所以始終顯示7個方格。

總結(jié)

本文主要是介紹了SwiftUI的布局思想觉吭,并介紹了幾種常見的基本View的布局原則腾供,同時介紹了.frame修飾器的內(nèi)部實現(xiàn)原理,同時利用實際例子進(jìn)行了演示,只有理解SwiftUI的布局思想台腥,才不會對意想不到的布局效果感到意外。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绒北,一起剝皮案震驚了整個濱河市黎侈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌闷游,老刑警劉巖峻汉,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異脐往,居然都是意外死亡休吠,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門业簿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瘤礁,“玉大人,你說我怎么就攤上這事梅尤」袼迹” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵巷燥,是天一觀的道長赡盘。 經(jīng)常有香客問我,道長缰揪,這世上最難降的妖魔是什么陨享? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮钝腺,結(jié)果婚禮上抛姑,老公的妹妹穿的比我還像新娘。我一直安慰自己拍屑,他們只是感情好途戒,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著僵驰,像睡著了一般喷斋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蒜茴,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天星爪,我揣著相機與錄音,去河邊找鬼粉私。 笑死顽腾,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播抄肖,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼久信,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了漓摩?” 一聲冷哼從身側(cè)響起裙士,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎管毙,沒想到半個月后腿椎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡夭咬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年啃炸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卓舵。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡南用,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出边器,到底是詐尸還是另有隱情训枢,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布忘巧,位于F島的核電站恒界,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏砚嘴。R本人自食惡果不足惜十酣,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望际长。 院中可真熱鬧耸采,春花似錦、人聲如沸工育。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽如绸。三九已至嘱朽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間怔接,已是汗流浹背搪泳。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扼脐,地道東北人岸军。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親艰赞。 傳聞我的和親對象是個殘疾皇子佣谐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容