Wie kann ich UIStackView mit variablem Abstand zwischen Ansichten erstellen?

70

Ich habe eine einfache Horizontale UIStackViewmit mehreren darin gestapelten UIViews. Mein Ziel ist es, einen variablen Abstand zwischen Ansichten zu schaffen. Mir ist klar, dass ich mit der Eigenschaft "Abstand" einen konstanten Abstand zwischen den Unteransichten erstellen kann. Mein Ziel ist es jedoch, variablen Raum zu schaffen. Bitte beachten Sie, dass ich nach Möglichkeit vermeiden möchte, unsichtbare Ansichten zu verwenden, die als Abstandshalter dienen.

Das Beste, was ich mir ausgedacht habe, war, meine UIViewsin eine separate Datei zu wickeln UIStackViewund die layoutMarginsRelativeArrangement = YESLayoutränder meines inneren Stapels zu respektieren. Ich hatte gehofft, mit jedem etwas Ähnliches machen zu können, UIViewohne auf diese hässliche Umgehung zurückzugreifen. Hier ist mein Beispielcode:

// Create stack view
UIStackView *stackView = [[UIStackView alloc] init];
stackView.translatesAutoresizingMaskIntoConstraints = NO;
stackView.axis = UILayoutConstraintAxisHorizontal;
stackView.alignment = UIStackViewAlignmentCenter;
stackView.layoutMarginsRelativeArrangement = YES;

// Create subview
UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
// ... Add Auto Layout constraints for height / width
// ...
// I was hoping the layoutMargins would be respected, but they are not
view1.layoutMargins = UIEdgeInsetsMake(0, 25, 0, 0);

// ... Create more subviews
// UIView view2 = [[UIView alloc] init];
// ...

// Stack the subviews
[stackView addArrangedSubview:view1];
[stackView addArrangedSubview:view2];

Das Ergebnis ist ein Stapel mit Ansichten direkt nebeneinander mit Abstand:

Geben Sie hier die Bildbeschreibung ein

Yura
quelle

Antworten:

137

Update Für iOS 11 StackViews mit benutzerdefiniertem Abstand

Apple hat die Möglichkeit hinzugefügt, benutzerdefinierte Abstände in iOS 11 festzulegen. Sie müssen lediglich den Abstand nach jeder angeordneten Unteransicht angeben. Leider können Sie vorher keinen Abstand angeben.

stackView.setCustomSpacing(10.0, after: firstLabel)
stackView.setCustomSpacing(10.0, after: secondLabel)

Immer noch viel besser als Ihre eigenen Ansichten zu verwenden.

Für iOS 10 und darunter

Sie können Ihrer Stapelansicht einfach transparente Ansichten hinzufügen und ihnen Breitenbeschränkungen hinzufügen.

(Label - UIView - Label - UIView - Label)

Wenn Sie weiterhin distributionfüllen, können Sie Einschränkungen für die variable Breite Ihrer UIViews festlegen.

Aber ich würde überlegen, ob dies die richtige Situation ist, um Stackviews zu verwenden, wenn dies der Fall ist. Autolayout macht es sehr einfach, variable Breiten zwischen Ansichten einzurichten.

Rob Norback
quelle
2
Vielen Dank, aber ich hatte gehofft, dass ich nicht auf leere Ansichten zurückgreifen musste, um Platz hinzuzufügen oder zum automatischen Layout zurückzukehren. Wenn UIStackView meinen Anwendungsfall nicht unterstützt, muss ich dies möglicherweise trotzdem tun.
Yura
2
Warum soll der Abstand variabel sein? Sie können einen variablen Abstand haben, wenn Sie 'Verteilung' 'Gleiche Zentrierung' verwenden. Dadurch werden die Mitten gleich breit, aber der Abstand variiert. Erklären Sie mehr über Ihren Anwendungsfall.
Rob Norback
2
Grundsätzlich habe ich mehrere Ansichten, die ich horizontal (oder vertikal) mit einem vorgegebenen Abstand zwischen den Ansichten verteilen muss, die möglicherweise denselben Wert haben oder nicht. Ich möchte in der Lage sein, diese Ränder entweder mit layoutMargins oder einem anderen Mechanismus anzugeben.
Yura
Lassen Sie mich wissen, ob Sie einen anderen Weg finden, aber ich glaube, dass das Festlegen von Einschränkungen für UIViews in einer Stapelansicht die einfachste Lösung ist.
Rob Norback
Geht klar! Danke für Ihre Hilfe. Zur Verdeutlichung Ihres Kommentars "Ich glaube, das Festlegen von Einschränkungen für UIViews in einer Stapelansicht ist die einfachste Lösung" - beziehen Sie sich auf Ihren ursprünglichen Vorschlag, leere Ansichten zwischen sichtbare Ansichten einzufügen?
Yura
5

SWIFT 4

Nach der Antwort von lilpit finden Sie hier eine Erweiterung von UIStackView, um Ihrer arrangierten Unteransicht einen oberen und einen unteren Abstand hinzuzufügen

extension UIStackView {
    func addCustomSpacing(top: CGFloat, bottom: CGFloat) {

        //If the stack view has just one arrangedView, we add a dummy one
        if self.arrangedSubviews.count == 1 {
            self.insertArrangedSubview(UIView(frame: .zero), at: 0)
        }

        //Getting the second last arrangedSubview and the current one
        let lastTwoArrangedSubviews = Array(self.arrangedSubviews.suffix(2))
        let arrSpacing: [CGFloat] = [top, bottom]

        //Looping through the two last arrangedSubview to add spacing in each of them
        for (index, anArrangedSubview) in lastTwoArrangedSubviews.enumerated() {

            //After iOS 11, the stackview has a native method
            if #available(iOS 11.0, *) {
                self.setCustomSpacing(arrSpacing[index], after: anArrangedSubview)
                //Before iOS 11 : Adding dummy separator UIViews
            } else {
                guard let arrangedSubviewIndex = arrangedSubviews.firstIndex(of: anArrangedSubview) else {
                    return
                }

                let separatorView = UIView(frame: .zero)
                separatorView.translatesAutoresizingMaskIntoConstraints = false

                //calculate spacing to keep a coherent spacing with the ios11 version
                let isBetweenExisitingViews = arrangedSubviewIndex != arrangedSubviews.count - 1
                let existingSpacing = isBetweenExisitingViews ? 2 * spacing : spacing
                let separatorSize = arrSpacing[index] - existingSpacing

                guard separatorSize > 0 else {
                    return
                }

                switch axis {
                case .horizontal:
                    separatorView.widthAnchor.constraint(equalToConstant: separatorSize).isActive = true
                case .vertical:
                    separatorView.heightAnchor.constraint(equalToConstant: separatorSize).isActive = true
                }

                insertArrangedSubview(separatorView, at: arrangedSubviewIndex + 1)
            }
        }
    }
}

Dann würden Sie es so verwenden:

//Creating label to add to the UIStackview
let label = UILabel(frame: .zero)

//Adding label to the UIStackview
stackView.addArrangedSubview(label)

//Create margin on top and bottom of the UILabel
stackView.addCustomSpacing(top: 40, bottom: 100)
Ugo Marinelli
quelle
2

Aus Robs Antwort habe ich eine UIStackView- Erweiterung erstellt, die helfen könnte:

extension UIStackView {
  func addCustomSpacing(_ spacing: CGFloat, after arrangedSubview: UIView) {
    if #available(iOS 11.0, *) {
      self.setCustomSpacing(spacing, after: arrangedSubview)
    } else {
      let separatorView = UIView(frame: .zero)
      separatorView.translatesAutoresizingMaskIntoConstraints = false
      switch axis {
      case .horizontal:
        separatorView.widthAnchor.constraint(equalToConstant: spacing).isActive = true
      case .vertical:
        separatorView.heightAnchor.constraint(equalToConstant: spacing).isActive = true
      }
      if let index = self.arrangedSubviews.firstIndex(of: arrangedSubview) {
        insertArrangedSubview(separatorView, at: index + 1)
      }
    }
  }
}

Sie können es verwenden und ändern, wie Sie möchten. Wenn Sie beispielsweise die Referenz " separatorView " möchten , können Sie einfach die UIView zurückgeben:

  func addCustomSpacing(_ spacing: CGFloat, after arrangedSubview: UIView) -> UIView?
Enrique
quelle
1
Dies funktioniert nicht, wenn Sie bereits einen Abstand in Ihrer stackView definiert haben (in diesem Fall funktioniert die ios 11-Version wie erwartet, aber die ios10-Version hat einen anderen Abstand (2 * defaultSpacing + Abstand)
lilpit
Wenn Sie einen benutzerdefinierten Abstand haben möchten, sollten Sie die Abstandseigenschaft nicht verwenden. Außerdem müssen SiestackView.alignment = .fill
Enrique
Ich verstehe Ihre Antwort nicht, mit setCustomSpacingist es möglich, die Abstandseigenschaft für Indizes zu verwenden, die Sie nicht verwendet haben setCustomSpacing, also ist meine Antwort richtig
Lilpit
1
Wie nicht? Sie haben meine Antwort kopiert und eingefügt und dieselbe verwendet setCustomSpacing. Außerdem haben Sie die Namen und Dinge des Ortes geändert, um eine andere Antwort zu erhalten.
Enrique
0

Um iOS 11.x und niedriger zu unterstützen, habe ich das UIStackView wie von Enrique erwähnt erweitert, es jedoch so geändert, dass es Folgendes enthält:

  • Hinzufügen eines Leerzeichens vor der arrangierten Unteransicht
  • Behandlung von Fällen, in denen bereits ein Speicherplatz vorhanden ist und nur aktualisiert werden muss
  • Zusätzlichen Speicherplatz entfernen
extension UIStackView {

    func addSpacing(_ spacing: CGFloat, after arrangedSubview: UIView) {
        if #available(iOS 11.0, *) {
            setCustomSpacing(spacing, after: arrangedSubview)
        } else {

            let index = arrangedSubviews.firstIndex(of: arrangedSubview)

            if let index = index, arrangedSubviews.count > (index + 1), arrangedSubviews[index + 1].accessibilityIdentifier == "spacer" {

                arrangedSubviews[index + 1].updateConstraint(axis == .horizontal ? .width : .height, to: spacing)
            } else {
                let separatorView = UIView(frame: .zero)
                separatorView.accessibilityIdentifier = "spacer"
                separatorView.translatesAutoresizingMaskIntoConstraints = false

                switch axis {
                case .horizontal:
                    separatorView.widthAnchor.constraint(equalToConstant: spacing).isActive = true
                case .vertical:
                    separatorView.heightAnchor.constraint(equalToConstant: spacing).isActive = true
                @unknown default:
                    return
                }
                if let index = index {
                    insertArrangedSubview(separatorView, at: index + 1)
                }
            }
        }
    }

    func addSpacing(_ spacing: CGFloat, before arrangedSubview: UIView) {

        let index = arrangedSubviews.firstIndex(of: arrangedSubview)

        if let index = index, index > 0, arrangedSubviews[index - 1].accessibilityIdentifier == "spacer" {

            let previousSpacer = arrangedSubviews[index - 1]

            switch axis {
            case .horizontal:
                previousSpacer.updateConstraint(.width, to: spacing)
            case .vertical:
                previousSpacer.updateConstraint(.height, to: spacing)
            @unknown default: return // Incase NSLayoutConstraint.Axis is extended in future
            }
        } else {
            let separatorView = UIView(frame: .zero)
            separatorView.accessibilityIdentifier = "spacer"
            separatorView.translatesAutoresizingMaskIntoConstraints = false

            switch axis {
            case .horizontal:
                separatorView.widthAnchor.constraint(equalToConstant: spacing).isActive = true
            case .vertical:
                separatorView.heightAnchor.constraint(equalToConstant: spacing).isActive = true
            @unknown default:
                return
            }
            if let index = index {
                insertArrangedSubview(separatorView, at: max(index - 1, 0))
            }
        }

    }

    func removeSpacing(after arrangedSubview: UIView) {
        if #available(iOS 11.0, *) {
            setCustomSpacing(0, after: arrangedSubview)
        } else {
            if let index = arrangedSubviews.firstIndex(of: arrangedSubview), arrangedSubviews.count > (index + 1), arrangedSubviews[index + 1].accessibilityIdentifier == "spacer" {
                arrangedSubviews[index + 1].removeFromStack()
            }
        }
    }

    func removeSpacing(before arrangedSubview: UIView) {
        if let index = arrangedSubviews.firstIndex(of: arrangedSubview), index > 0, arrangedSubviews[index - 1].accessibilityIdentifier == "spacer" {
            arrangedSubviews[index - 1].removeFromStack()
        }
    }
}


extension UIView {
    func updateConstraint(_ attribute: NSLayoutConstraint.Attribute, to constant: CGFloat) {
        for constraint in constraints {
            if constraint.firstAttribute == attribute {
              constraint.constant = constant
            }
        }
    }

    func removeFromStack() {
        if let stack = superview as? UIStackView, stack.arrangedSubviews.contains(self) {
            stack.removeArrangedSubview(self)
            // Note: 1
            removeFromSuperview()
        }
    }
}

Hinweis: 1 - Gemäß der Dokumentation:

Um zu verhindern, dass die Ansicht nach dem Aufrufen der removeArrangedSubview: -Methode des Stapels auf dem Bildschirm angezeigt wird, entfernen Sie die Ansicht explizit aus dem Array subviews, indem Sie die removeFromSuperview () -Methode der Ansicht aufrufen, oder setzen Sie die isHidden-Eigenschaft der Ansicht auf true.

pimisi
quelle
0

Um ein ähnliches Verhalten wie CSS-Rand und Auffüllung zu erzielen.

  1. Polsterung

    myStackView.directionalLayoutMargins = NSDirectionalEdgeInsets (oben: oben, führend: links, unten: unten, nachlaufend: rechts);

  2. Rand (erstellen Sie eine Wrapper-Ansicht und fügen Sie dem Wrapper Polster hinzu)

        wrapper = UIStackView();
        wrapper!.frame = viewToAdd.frame;
        wrapper!.frame.size.height = wrapper!.frame.size.height + marginTop + marginBottom;
        wrapper!.frame.size.width = wrapper!.frame.size.width + marginLeft + marginRight;
        (wrapper! as! UIStackView).axis = .horizontal;
        (wrapper! as! UIStackView).alignment = .fill
        (wrapper! as! UIStackView).spacing = 0
        (wrapper! as! UIStackView).distribution = .fill
        wrapper!.translatesAutoresizingMaskIntoConstraints = false
    
        (wrapper! as! UIStackView).isLayoutMarginsRelativeArrangement = true;
        (wrapper! as! UIStackView).insetsLayoutMarginsFromSafeArea = false;
        wrapper!.directionalLayoutMargins = NSDirectionalEdgeInsets(top: marginTop, leading: marginLeft, bottom: marginBottom, trailing: marginRight);wrapper.addArrangedSubview(viewToAdd);
    
Milfar Dean
quelle
0

Wenn Sie die vorherige Ansicht nicht kennen, können Sie eine eigene UIView mit Abstand erstellen und diese als angeordnete Unteransicht zu Ihrer Stapelansicht hinzufügen.

func spacing(value: CGFloat) -> UIView {
    let spacerView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
    spacerView.translatesAutoresizingMaskIntoConstraints = false
    spacerView.heightAnchor.constraint(equalToConstant: value).isActive = true
    return spacerView
}
stackView.addArrangedSubview(spacing(value: 16))
Dan
quelle