如何在 Swift 中的集合视图上方添加水平按钮堆栈?

Posted

技术标签:

【中文标题】如何在 Swift 中的集合视图上方添加水平按钮堆栈?【英文标题】:How can I add a horizontal stack of buttons above a collection view in Swift? 【发布时间】:2020-10-02 15:54:13 【问题描述】:

我正在尝试在集合视图上方添加三个按钮,根据用户选择的按钮从照片库中提取视频、照片或所有内容。

我设法分别创建了集合视图和按钮,但是当我尝试将它们组合到垂直堆栈视图中时,我在声明 UIStackView 时遇到错误。

我正在尝试将水平堆栈视图(按钮)组合成垂直堆栈视图(顶部的按钮堆栈视图和下方的集合视图)。

我认为该错误与我声明 buttonStack 的方式有关,但我尝试的一切都失败了。

我认为两个堆叠的视图将是实现我的目标的最佳方式,但我愿意接受其他/更好的建议。无论如何,我想知道为什么这对我不起作用。

代码行和错误信息:

让 stackView = UIStackView(arrangedSubviews: [buttonsStack, collectionView!])

没有更多上下文的表达类型是模棱两可的

class TestVideoItemCell: UICollectionViewCell 
    
    var stackView = UIStackView()
    var vid = UIImageView()
    
    override init(frame: CGRect) 
        super.init(frame: frame)
        vid.contentMode = .scaleAspectFill
        vid.clipsToBounds = true
        self.addSubview(vid)
    
    override func layoutSubviews() 
        super.layoutSubviews()
        vid.frame = self.bounds
    
    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    


class TestVideoViewVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate 
    
    var myCollectionView: UICollectionView!
    var videoArray = [UIImage]()
    
    override func viewDidLoad() 
        super.viewDidLoad()
        grabVideos()
    
    
    //MARK: CollectionView
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
        return videoArray.count
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoCell", for: indexPath) as! VideoItemCell
        cell.vid.image = videoArray[indexPath.item]
        return cell
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 
        let width = collectionView.frame.width
        return CGSize(width: width/4 - 1, height: width/4 - 1)
    
    override func viewWillLayoutSubviews() 
        super.viewWillLayoutSubviews()
        myCollectionView.collectionViewLayout.invalidateLayout()
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat 
        return 1.0
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat 
        return 1.0
    
    
    //MARK: grab videos
    func grabVideos()
        videoArray = []
        
        DispatchQueue.global(qos: .background).async 
            let imgManager = PHImageManager.default()
            
            let requestOptions = PHImageRequestOptions()
            requestOptions.isSynchronous = true
            requestOptions.deliveryMode = .highQualityFormat
            
            let fetchOptions = PHFetchOptions()
            fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: false)]
            
            let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .video, options: fetchOptions)
            print(fetchResult)
            print(fetchResult.count)
            if fetchResult.count > 0 
                for i in 0..<fetchResult.count
                    imgManager.requestImage(for: fetchResult.object(at: i) as PHAsset, targetSize: CGSize(width:500, height: 500),contentMode: .aspectFill, options: requestOptions, resultHandler:  (image, error) in
                        self.videoArray.append(image!)
                    )
                
             else 
                print("No videos found.")
            
            DispatchQueue.main.async 

                self.myCollectionView.reloadData()
            
        
    
    
    override func didReceiveMemoryWarning() 
        super.didReceiveMemoryWarning()
    
    
    //    MARK: SetUp Methods
    func buttonsStack() 
        let buttonChoices = ButtonStackView()
        buttonChoices.setupButtonsStackView()
    
    func setupCollection() 
        let layout = UICollectionViewFlowLayout()
        myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
        myCollectionView.delegate = self
        myCollectionView.dataSource = self
        myCollectionView.register(VideoItemCell.self, forCellWithReuseIdentifier: "videoCell")
        myCollectionView.backgroundColor = UIColor.white
        self.view.addSubview(myCollectionView)
        
        myCollectionView.autoresizingMask = UIView.AutoresizingMask(rawValue: UIView.AutoresizingMask.RawValue(UInt8(UIView.AutoresizingMask.flexibleWidth.rawValue) | UInt8(UIView.AutoresizingMask.flexibleHeight.rawValue)))
        
    
    func setupStack() 
        let stackView = UIStackView(arrangedSubviews: [buttonsStack, collectionView!])
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.distribution = .fillEqually
        stackView.axis = .horizontal
        stackView.spacing = 8
        
        view.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
        ])
    

更新 下面是我正在尝试创建的 UI 的一般概念的图像示例。

【问题讨论】:

不使用情节提要??? 不,我正在尝试尽可能以编程方式进行。我知道使用界面构建器更容易,但我认为长期学习如何以编程方式完成它会更好,但我对 Swift 太陌生了,我不确定实现这一点的最佳方法。界面构建器会更好吗? 这当然不是“更容易!”用故事板来做!这是一种真正的艺术形式。如果您的目标最终是在商业上工作,那么大笔资金就是掌握故事板。毫无疑问,您必须在这两个方面都绝对是专家:/ 在这个时代,你真的不能“设置框架”,比如你的代码在 .. vid.frame = self.bounds。您必须在任何地方使用约束(即自动布局)。老实说不幸的是,您可能必须从头开始,到处都有限制。 (只有在低级别工作时才“设置框架”,使用 CALayers 制作完全自定义的视图,示例***.com/a/41962342/294884(查看底部的链接)) 【参考方案1】:

试着一步一步来……

从此代码开始(分配给普通的UIViewController 作为UINavigationController 的根控制器):

class TestVideoViewVC: UIViewController 
    
    var myCollectionView: UICollectionView!
    var videoArray = [UIImage]()
    
    override func viewDidLoad() 
        super.viewDidLoad()
        
        // set main view background color to a nice medium blue
        view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
        
        // vertical stack view for the full screen (safe area)
        let mainStack = UIStackView()
        mainStack.axis = .vertical
        mainStack.spacing = 8
        mainStack.translatesAutoresizingMaskIntoConstraints = false
        
        // add it to the view
        view.addSubview(mainStack)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            mainStack.topAnchor.constraint(equalTo: g.topAnchor),
            mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
    


如果您按原样运行应用程序,您会看到:

堆栈视图在那里,但我们还没有添加任何子视图。

所以,让我们添加两个视图(标签)...顶部的高度为 50 点:

class TestVideoViewVC: UIViewController 
    
    var myCollectionView: UICollectionView!
    var videoArray = [UIImage]()
    
    override func viewDidLoad() 
        super.viewDidLoad()
        
        // set main view background color to a nice medium blue
        view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
        
        // vertical stack view for the full screen (safe area)
        let mainStack = UIStackView()
        mainStack.axis = .vertical
        mainStack.spacing = 8
        mainStack.translatesAutoresizingMaskIntoConstraints = false
        
        // add it to the view
        view.addSubview(mainStack)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            mainStack.topAnchor.constraint(equalTo: g.topAnchor),
            mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        // add two arranged subviews, so we can see the layout
        let v1 = UILabel()
        v1.textAlignment = .center
        v1.text = "Buttons will go here..."
        v1.backgroundColor = .green
        
        let v2 = UILabel()
        v2.textAlignment = .center
        v2.text = "Collection view will go here..."
        v2.backgroundColor = .yellow
        
        // let's give the top view a height of 50-pts
        v1.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
        
        mainStack.addArrangedSubview(v1)
        mainStack.addArrangedSubview(v2)
    


运行该代码,我们得到:

现在,让我们将v1(“顶部”标签)替换为带有 3 个红色、50 磅高的按钮的水平堆栈视图:

class TestVideoViewVC: UIViewController 
    
    var myCollectionView: UICollectionView!
    var videoArray = [UIImage]()
    
    override func viewDidLoad() 
        super.viewDidLoad()
        
        // set main view background color to a nice medium blue
        view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
        
        // vertical stack view for the full screen (safe area)
        let mainStack = UIStackView()
        mainStack.axis = .vertical
        mainStack.spacing = 8
        mainStack.translatesAutoresizingMaskIntoConstraints = false
        
        // add it to the view
        view.addSubview(mainStack)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            mainStack.topAnchor.constraint(equalTo: g.topAnchor),
            mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        // create a horizontal stack view
        let buttonsStack = UIStackView()
        buttonsStack.axis = .horizontal
        buttonsStack.spacing = 8
        buttonsStack.distribution = .fillEqually

        // create and add 3 50-pt height buttons to the stack view
        ["Videos", "Photos", "All"].forEach  str in
            let b = UIButton()
            b.setTitle(str, for: [])
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.gray, for: .highlighted)
            b.backgroundColor = .red
            buttonsStack.addArrangedSubview(b)
            b.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
        
        
        // add the buttons stack view to the main stack view
        mainStack.addArrangedSubview(buttonsStack)
        
        // create a label (this will be our collection view)
        let v2 = UILabel()
        v2.textAlignment = .center
        v2.text = "Collection view will go here..."
        v2.backgroundColor = .yellow

        // add the label to the main stack view
        mainStack.addArrangedSubview(v2)
    


运行该代码,我们得到:

现在我们可以用我们的集合视图替换v2

class TestVideoViewVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate 
    
    var myCollectionView: UICollectionView!
    var videoArray = [UIImage]()
    
    override func viewDidLoad() 
        super.viewDidLoad()
        
        // set main view background color to a nice medium blue
        view.backgroundColor = UIColor(red: 0.25, green: 0.5, blue: 1.0, alpha: 1.0)
        
        // vertical stack view for the full screen (safe area)
        let mainStack = UIStackView()
        mainStack.axis = .vertical
        mainStack.spacing = 8
        mainStack.translatesAutoresizingMaskIntoConstraints = false
        
        // add it to the view
        view.addSubview(mainStack)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            mainStack.topAnchor.constraint(equalTo: g.topAnchor),
            mainStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            mainStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            mainStack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        // create a horizontal stack view
        let buttonsStack = UIStackView()
        buttonsStack.axis = .horizontal
        buttonsStack.spacing = 8
        buttonsStack.distribution = .fillEqually

        // create and add 3 50-pt height buttons to the stack view
        ["Videos", "Photos", "All"].forEach  str in
            let b = UIButton()
            b.setTitle(str, for: [])
            b.setTitleColor(.white, for: .normal)
            b.setTitleColor(.gray, for: .highlighted)
            b.backgroundColor = .red
            buttonsStack.addArrangedSubview(b)
            b.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
        
        
        // add the buttons stack view to the main stack view
        mainStack.addArrangedSubview(buttonsStack)
        
        // setup the collection view
        setupCollection()
        
        // add it to the main stack view
        mainStack.addArrangedSubview(myCollectionView)
        
        // start the async call to get the assets
        grabVideos()
    

    func setupCollection() 
        let layout = UICollectionViewFlowLayout()
        myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
        myCollectionView.delegate = self
        myCollectionView.dataSource = self
        myCollectionView.register(VideoItemCell.self, forCellWithReuseIdentifier: "videoCell")
        myCollectionView.backgroundColor = UIColor.white
    

    //MARK: CollectionView
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
        return videoArray.count
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoCell", for: indexPath) as! VideoItemCell
        cell.vid.image = videoArray[indexPath.item]
        return cell
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 
        let width = collectionView.frame.width
        return CGSize(width: width/4 - 1, height: width/4 - 1)
    
    override func viewWillLayoutSubviews() 
        super.viewWillLayoutSubviews()
        myCollectionView.collectionViewLayout.invalidateLayout()
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat 
        return 1.0
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat 
        return 1.0
    
    
    //MARK: grab videos
    func grabVideos()
        videoArray = []
        
        DispatchQueue.global(qos: .background).async 
            let imgManager = PHImageManager.default()
            
            let requestOptions = PHImageRequestOptions()
            requestOptions.isSynchronous = true
            requestOptions.deliveryMode = .highQualityFormat
            
            let fetchOptions = PHFetchOptions()
            fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: false)]
            
            //let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .video, options: fetchOptions)
            let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
            print(fetchResult)
            print(fetchResult.count)
            if fetchResult.count > 0 
                for i in 0..<fetchResult.count
                    imgManager.requestImage(for: fetchResult.object(at: i) as PHAsset, targetSize: CGSize(width:500, height: 500),contentMode: .aspectFill, options: requestOptions, resultHandler:  (image, error) in
                        self.videoArray.append(image!)
                    )
                
             else 
                print("No videos found.")
            
            DispatchQueue.main.async 
                
                self.myCollectionView.reloadData()
            
        
    
    


class VideoItemCell: UICollectionViewCell 
    
    var stackView = UIStackView()
    var vid = UIImageView()
    
    override init(frame: CGRect) 
        super.init(frame: frame)
        vid.contentMode = .scaleAspectFill
        vid.clipsToBounds = true
        self.addSubview(vid)
    
    override func layoutSubviews() 
        super.layoutSubviews()
        vid.frame = self.bounds
    
    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

我们应该有这个(我只有默认照片,没有视频,所以我更改了grabVideos() 来获取照片):

【讨论】:

我对 Swift 很陌生(您可能已经注意到了),但是您已经针对我提出或研究过的问题发布了几个答案。您的回答和解释非常有帮助!非常感谢你!最初我对你的答案有类似的方法,但我无法让它发挥作用。那是我开始尝试不同的事情并以 OP 中发布的意大利面条代码结束的时候。我现在看到我的原始问题与我使用 .addArrangedSubview 的方式有关。再次感谢!!【参考方案2】:

使用 AutoLayout 而不是垂直 UIStackView 可能会更好。

因此,您应该将 Button 堆栈视图放在 ViewController 的顶部,并且您可能希望给它一个固定的高度。然后,不是设置collectionView的框架,而是设置它的约束,使其边缘粘在viewController的视图上,除了顶部约束,它应该粘在按钮堆栈的底部约束。

旁注,界面生成器不一定比以编程方式创建视图更好。就个人而言,我建议您坚持编程方式。

【讨论】:

您必须使用情节提要完全掌握和理解各处的约束(自动布局)。然后(如果出于某种原因你想)你可以在代码中完成这一切。 理解约束实际上并不难,除非你是一个完全的初学者。每个初级开发人员都应该能够做到这一点。【参考方案3】:

好吧,我建议以编程方式进行。但是,我不太了解您的代码,但我建议您可以将UIStackView 放在安全区域下方,然后会有UICollectionView

private let stackV: UIStackView = 

   let stackV = UIStackView()
   stackV.axis = .horizontal
   stackV.distribution = .horizontal
   stackV.distribution = .equalSpacing


   return stackV
()

然后,只需创建按钮:

private let collectionB: [UIButtons] =  

  let buttonA = UIButton()

  let buttonB = UIButton()

  let buttonC = UIButton()

  return [buttonA, buttonB, buttonC]
()

您可以将其作为排列好的子视图添加到stackV

collectionB.forEach  stackV.addArrangedSubview($0) 

这样就可以了,你必须给stackV 一个固定的高度,然后你可以在底部添加collectionV 并相应地放置你的约束。

我已经以编程方式创建视图很长一段时间了,而您想要实现的目标是完全可以实现的,如果我看到 UI 那么事情会更好,我不明白的是为什么你有没有在你的UICollectionViewCell中加了stackView,可能处理起来更流畅。

P.S.:我没有在 Xcode 上输入此代码,它可能会引发一些错误。

【讨论】:

虽然这段代码很好,但需要注意的一点是,在现实生活中的应用程序中,您可以只使用“:固定高度”是不寻常的。 (请注意,我什至无法遵循 OP 正在尝试做的事情 :)) @Rob 感谢您的解释。我现在正在尝试一些解决方案。我知道我的代码和解释/问题可能令人困惑(我显然是新手)。感谢您的回复。它使我的问题变得清晰。我刚刚用我想要实现的 UI 模型更新了我的问题。

以上是关于如何在 Swift 中的集合视图上方添加水平按钮堆栈?的主要内容,如果未能解决你的问题,请参考以下文章

swift 3 - 自定义集合视图单元格之间的水平空间

如何在 iOS swift 中的 ARSCNView 中显示 uicollection 视图

Swift:以编程方式在键盘上方添加 UIButton

如何将数组中的非静态图像加载到 Swift 中的集合视图中

按钮是不可点击的 swift

将按钮添加到 UICollectionView 单元格