英文:
UITextView doesn't display all of the text on small screens when using exclusions
问题
我有一个UITextView:
var textView: UITextView = {
let textView = UITextView()
textView.textColor = .orange
textView.isScrollEnabled = false
textView.isEditable = false
return textView
}()
我将它与另一个视图进行了约束,而这个视图又是UIStackView的一部分:
mainStack.axis = .vertical
mainStack.addArrangedSubview(titleView)
mainStack.addArrangedSubview(bodyView)
titleView.addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = false
let padding: CGFloat = 5
NSLayoutConstraint.activate([
textView.leadingAnchor.constraint(equalTo: titleView.leadingAnchor, constant: padding),
textView.trailingAnchor.constraint(equalTo: titleView.trailingAnchor, constant: -padding),
textView.topAnchor.constraint(equalTo: titleView.topAnchor, constant: padding),
textView.bottomAnchor.constraint(equalTo: titleView.bottomAnchor, constant: -padding)
])
此外,我还有一个部分约束在相同的区域内的按钮:
let mainActionButton = UIButton()
titleView.addSubview(mainActionButton)
mainActionButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
mainActionButton.trailingAnchor.constraint(equalTo: titleView.trailingAnchor),
mainActionButton.topAnchor.constraint(equalTo: titleView.topAnchor),
mainActionButton.heightAnchor.constraint(equalTo: mainActionButton.widthAnchor)
])
在运行时,在viewDidLoad被调用后,我使用以下代码确保文本围绕按钮流动:
textView.textContainer.exclusionPaths = [UIBezierPath(rect: mainActionButton.frame)]
self.layoutSubviews()
总的来说,这个方法效果很好,如下面的屏幕截图所示:
然而,当我将UITextView的约束中的padding值改为小于4时,在仅限小屏幕上,它不会显示最后一行,如下图所示:
再次强调,只有在以下情况下才会发生这种情况:
- 没有排除路径(按钮)
- 在大屏幕上
- padding大于3像素
有人知道为什么会出现这种情况吗?
英文:
I have a UITextView:
var textView: UITextView = {
let textView = UITextView()
textView.textColor = .orange
textView.isScrollEnabled = false
textView.isEditable = false
return textView
}()
I have it constrained to another view, which is in turn part of a UIStackView
mainStack.axis = .vertical
mainStack.addArrangedSubview(titleView)
mainStack.addArrangedSubview(bodyView)
titleView.addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = false
let padding: CGFloat = 5
NSLayoutConstraint.activate([
textView.leadingAnchor.constraint(equalTo: titleView.leadingAnchor, constant: padding),
textView.trailingAnchor.constraint(equalTo: titleView.trailingAnchor, constant: -padding),
textView.topAnchor.constraint(equalTo: titleView.topAnchor, constant: padding),
textView.bottomAnchor.constraint(equalTo: titleView.bottomAnchor, constant: -padding)
])
In addition I have a button constrained partially in the same area
let mainActionButton = UIButton()
titleView.addSubview(mainActionButton)
mainActionButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
mainActionButton.trailingAnchor.constraint(equalTo: titleView.trailingAnchor),
mainActionButton.topAnchor.constraint(equalTo: titleView.topAnchor),
mainActionButton.heightAnchor.constraint(equalTo: mainActionButton.widthAnchor)
])
At runtime, after viewDidLoad is called, I use the following to make sure the text flows around the button
textView.textContainer.exclusionPaths = [UIBezierPath(rect: mainActionButton.frame)]
self.layoutSubviews()
Generally, this works nicely, as you can see from the screenshot:
However, when I changed the padding to anything less than 4 in the constraints for the uitextview, on small screens only, it does not display the last line, as you can see below:
Again, this doesn't happen if:
- There is no exclusion path (button)
- It is on a large screen
- The padding is more than 3 px
Does anyone have any idea why this might be happening?
答案1
得分: 0
我不确定为什么会出现这种情况,但我认为原因是我将UITextView放在一个同时位于UIScrollView中的stackview中,因此它从未获得任何内容大小的明确定义。
我所做的是创建一个在视图加载后调用的方法,该方法使用'systemLayoutSizeFitting'计算位于相同位置的标签的内在内容大小(当然要减去任何额外的填充/宽度)。我取得了高度值并设置了一个硬约束。由于某种原因,我不得不添加30才能使其正常工作 - 文本字段可能还有其他方面添加了30像素的填充。
func layoutTitle() {
let label = UILabel()
label.font = Fonts.titleFont
label.numberOfLines = -1
label.text = titleTextView.text
let size = CGSize(width: self.frame.width - (padding * 2) - mainActionButton.frame.width, height: 0)
let newSize = label.systemLayoutSizeFitting(size, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
var height = newSize.height + 30
titleTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: height).isActive = true
}
注意:我在下面添加了一个最小可重现示例;你可以将其放入单个视图应用程序的视图控制器文件中。棘手的部分是不同长度的字符串在不同的手机上工作,但如果你运行iPhone 14 Pro Max模拟器,你会看到只有到数字16才会出现,隐藏了17、18和19。如果你将'20'添加到titleTextView的文本中,它将再次出现。
import UIKit
class ViewController: UIViewController {
let primaryView = PrimaryView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.constrain(primaryView)
}
override func viewDidLayoutSubviews() {
primaryView.layoutTitle()
}
}
class PrimaryView: UIView {
var viewController: UIViewController!
var mainScroll = UIScrollView()
var mainStack: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
return stackView
} ()
var titleTextView: UITextView = {
let textView = UITextView()
textView.font = UIFont.systemFont(ofSize: 36)
textView.isScrollEnabled = false
textView.isEditable = false
return textView
}()
var mainActionButtonArea = UIView()
var mainActionButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(systemName: "square.and.pencil.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 36)), for: .normal)
return button
}()
var bodyView = UIStackView()
init() {
super.init(frame: .zero)
setUpMainStack()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setUpMainStack() {
// Constrain scrollview to view
self.constrain(mainScroll, padding: 30)
// Constrain UIStackView to scroll view
mainScroll.constrain(mainStack)
mainScroll.widthAnchor.constraint(equalTo: mainStack.widthAnchor).isActive = true
// Add child views
mainStack.addArrangedSubview(titleTextView)
mainStack.addArrangedSubview(bodyView)
titleTextView.text = "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19"
// Add action button area + action button
mainScroll.constrain(mainActionButtonArea, except: [.leading, .bottom])
mainActionButtonArea.constrain(mainActionButton, padding: 10)
mainActionButton.heightAnchor.constraint(equalTo: mainActionButton.widthAnchor).isActive = true
}
func layoutTitle() {
// This adds the exception path; needs to be called after subviews are already laid out
let buttonPath = UIBezierPath(rect: mainActionButtonArea.frame)
titleTextView.textContainer.exclusionPaths = [buttonPath]
}
}
// Helper function
extension UIView {
enum ConstraintType: CaseIterable { case leading, trailing, top, bottom }
func constrain(_ child: UIView, padding: CGFloat = 0, except: [ConstraintType] = []) {
self.addSubview(child)
child.translatesAutoresizingMaskIntoConstraints = false
for type in ConstraintType.allCases where !except.contains(type) {
switch type {
case .leading:
child.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding).isActive = true
case .trailing:
child.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -padding).isActive = true
case .top:
child.topAnchor.constraint(equalTo: self.topAnchor, constant: padding).isActive = true
case .bottom:
child.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding).isActive = true
}
}
}
}
希望这可以帮助到你!
英文:
While I'm not 100% sure why this was happening, I think it's because I had placed the UITextView in a stackview that was also in a scrollview, and therefore it was never getting any hard definition of a content size.
What I did was create a method that is called after the view is loaded, which calculates the intrinsic content size of a label that would be in the same location using 'systemLayoutSizeFitting' (and of course subtracting any extra padding / width'). I took that height value and set a hard constraint. For some reason, I had to add 30 to make it work properly - there must be some other aspect of the textfield that adds 30 px of padding.
func layoutTitle() {
let label = UILabel()
label.font = Fonts.titleFont
label.numberOfLines = -1
label.text = titleTextView.text
let size = CGSize(width: self.frame.width - (padding * 2) - mainActionButton.frame.width, height: 0)
let newSize = label.systemLayoutSizeFitting(size, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
var height = newSize.height + 30
titleTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: height).isActive = true
}
NOTE I'm adding a minimum reproducible example below; you should be able to pop this into the view controller file of a single view app. The tricky part is that different lengths of string work on different phones, but if you run the iPhone 14 Pro Max simulator, you'll see that only up to the number 16 appears, hiding 17, 18, and 19. If you then add '20' to the titleTextView's text, it appears again.
import UIKit
class ViewController: UIViewController {
let primaryView = PrimaryView()
override func viewDidLoad() {
super.viewDidLoad()
self.view.constrain(primaryView)
}
override func viewDidLayoutSubviews() {
primaryView.layoutTitle()
}
}
class PrimaryView: UIView {
var viewController: UIViewController!
var mainScroll = UIScrollView()
var mainStack: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
return stackView
} ()
var titleTextView: UITextView = {
let textView = UITextView()
textView.font = UIFont.systemFont(ofSize: 36)
textView.isScrollEnabled = false
textView.isEditable = false
return textView
}()
var mainActionButtonArea = UIView()
var mainActionButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(systemName: "square.and.pencil.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 36)), for: .normal)
return button
}()
var bodyView = UIStackView()
init() {
super.init(frame: .zero)
setUpMainStack()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setUpMainStack() {
// Constrain scrollview to view
self.constrain(mainScroll, padding: 30)
// Constrain UIStackView to scroll view
mainScroll.constrain(mainStack)
mainScroll.widthAnchor.constraint(equalTo: mainStack.widthAnchor).isActive = true
// Add child views
mainStack.addArrangedSubview(titleTextView)
mainStack.addArrangedSubview(bodyView)
titleTextView.text = "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19"
// Add action button area + action button
mainScroll.constrain(mainActionButtonArea, except: [.leading, .bottom])
mainActionButtonArea.constrain(mainActionButton, padding: 10)
mainActionButton.heightAnchor.constraint(equalTo: mainActionButton.widthAnchor).isActive = true
}
func layoutTitle() {
// This adds the exception path; needs to be called after subviews are already laid out
let buttonPath = UIBezierPath(rect: mainActionButtonArea.frame)
titleTextView.textContainer.exclusionPaths = [buttonPath]
}
}
// Helper function
extension UIView {
enum ConstraintType: CaseIterable { case leading, trailing, top, bottom }
func constrain(_ child: UIView, padding: CGFloat = 0, except: [ConstraintType] = []) {
self.addSubview(child)
child.translatesAutoresizingMaskIntoConstraints = false
for type in ConstraintType.allCases where !except.contains(type) {
switch type {
case .leading:
child.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding).isActive = true
case .trailing:
child.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -padding).isActive = true
case .top:
child.topAnchor.constraint(equalTo: self.topAnchor, constant: padding).isActive = true
case .bottom:
child.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding).isActive = true
}
}
}
}
答案2
得分: 0
根据你的“最小可复现示例”...
我过去发现,在设置UITextView的.exclusionPaths时,需要“提示”布局进行更新。在设置排除路径之后设置.text
似乎更可靠。
此外,虽然与此无关,但在开发过程中,给UI元素提供对比的背景颜色非常有帮助,以便在运行时轻松查看框架。
所以,这是你的示例,几乎没有改动,只有以下更改:
- 将
PrimaryView
约束到安全区域 - 给
bodyView
设置高度,并添加一个标签作为其子视图进行标识 - 给所有视图设置背景颜色
你的“辅助”函数 - 未修改
// 辅助函数
extension UIView {
enum ConstraintType: CaseIterable { case leading, trailing, top, bottom }
func constrain(_ child: UIView, padding: CGFloat = 0, except: [ConstraintType] = []) {
self.addSubview(child)
child.translatesAutoresizingMaskIntoConstraints = false
for type in ConstraintType.allCases where !except.contains(type) {
switch type {
case .leading:
child.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding).isActive = true
case .trailing:
child.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -padding).isActive = true
case .top:
child.topAnchor.constraint(equalTo: self.topAnchor, constant: padding).isActive = true
case .bottom:
child.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding).isActive = true
}
}
}
}
视图控制器类 - 修改以遵守安全区域
class ExclusionTestViewController: UIViewController {
let primaryView = MyPrimaryView()
override func viewDidLoad() {
super.viewDidLoad()
//self.view.constrain(primaryView)
primaryView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(primaryView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
primaryView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
primaryView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
primaryView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
primaryView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
primaryView.layoutTitle()
}
}
PrimaryView
类 - 原始代码,除了添加颜色和bodyView属性
class MyPrimaryView: UIView {
var mainScroll = UIScrollView()
var mainStack: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
return stackView
} ()
var titleTextView: UITextView = {
let textView = UITextView()
textView.font = UIFont.systemFont(ofSize: 36)
textView.isScrollEnabled = false
textView.isEditable = false
return textView
}()
var mainActionButtonArea = UIView()
var mainActionButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(systemName: "square.and.pencil.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 36)), for: .normal)
return button
}()
var bodyView = UIStackView()
init() {
super.init(frame: .zero)
setUpMainStack()
// 设置背景颜色以便轻松查看框架
self.backgroundColor = .cyan
mainScroll.backgroundColor = .systemBlue
mainStack.backgroundColor = .systemYellow
titleTextView.backgroundColor = .green
mainActionButtonArea.backgroundColor = .red.withAlphaComponent(0.75)
mainActionButton.backgroundColor = .white
bodyView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
// 让bodyView具有高度约束为180
bodyView.heightAnchor.constraint(equalToConstant: 180.0).isActive = true
// 并添加一个标签进行标识
let v = UILabel()
v.font = .italicSystemFont(ofSize: 24.0)
v.text = "Body View"
bodyView.constrain(v, padding: 12.0, except: [.trailing, .bottom])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setUpMainStack() {
// 将滚动视图约束到视图
self.constrain(mainScroll, padding: 30)
// 将UIStackView约束到滚动视图
mainScroll.constrain(mainStack)
mainScroll.widthAnchor.constraint(equalTo: mainStack.widthAnchor).isActive = true
// 添加子视图
mainStack.addArrangedSubview(titleTextView)
mainStack.addArrangedSubview(bodyView)
titleTextView.text = "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19"
// 添加操作按钮区域+操作按钮
mainScroll.constrain(mainActionButtonArea, except: [.leading, .bottom])
mainActionButtonArea.constrain(mainActionButton, padding: 10)
mainActionButton.heightAnchor.constraint(equalTo: mainActionButton.widthAnchor).isActive = true
}
func layoutTitle() {
// 这将添加例外路径;需要在子视图已经布局之后调用
let buttonPath = UIBezierPath(rect: mainActionButtonArea.frame)
titleTextView.textContainer.exclusionPaths = [buttonPath]
// “重新设置”文本视图的文本并强制进行布局
let str: String = titleTextView.text ?? ""
titleTextView.text = ""
titleTextView.text = str
titleTextView.setNeedsLayout()
titleTextView.layoutIfNeeded()
}
}
现在它看起来像这样:
正如你指出的,缺少“17 18 19”行。
所以,让我们在设置排除路径时强制文本视图更新其布局。唯一的改动是在这里添加了几行代码:
func layoutTitle() {
// 这将添加例外路径;需要在子视图已经布局之后调用
let buttonPath = UIBezierPath(rect: mainActionButtonArea.frame)
titleTextView.textContainer.exclusionPaths = [buttonPath]
// “重新设置”文本视图的文本并强制进行布局
let str: String = titleTextView.text ?? ""
titleTextView.text = ""
titleTextView.text = str
titleTextView.setNeedsLayout()
titleTextView.layoutIfNeeded()
}
现在当我们运行它时,我们得到:
而且不需要创建一个临时标签来进行可能不可靠和灵活的尺寸计算。
顺便说一句,要使其与动态大小更改(例如设备旋转)“良好协作”,仍然需要做一些工作。
英文:
Based on your "minimum reproducible example" ...
I've found in the past that when setting .exclusionPaths
in a UITextView
, the layout needs to be "prompted" to update. It also seems much more reliable to set the .text
after setting the exclusion paths.
Also - while not related to what's going on here - I find it very helpful during development to give UI elements contrasting background colors to make it easy to see the framing at run-time.
So, here is your example, almost as-is, with these changes:
- constrained
PrimaryView
to the safe-area - gave
bodyView
a height, and added a label as a subview to identify it - gave all the views background colors
your "helper" function - unmodified
// Helper function
extension UIView {
enum ConstraintType: CaseIterable { case leading, trailing, top, bottom }
func constrain(_ child: UIView, padding: CGFloat = 0, except: [ConstraintType] = []) {
self.addSubview(child)
child.translatesAutoresizingMaskIntoConstraints = false
for type in ConstraintType.allCases where !except.contains(type) {
switch type {
case .leading:
child.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding).isActive = true
case .trailing:
child.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -padding).isActive = true
case .top:
child.topAnchor.constraint(equalTo: self.topAnchor, constant: padding).isActive = true
case .bottom:
child.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding).isActive = true
}
}
}
}
view controller class - modified to respect safe-area
class ExclusionTestViewController: UIViewController {
let primaryView = MyPrimaryView()
override func viewDidLoad() {
super.viewDidLoad()
//self.view.constrain(primaryView)
primaryView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(primaryView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
primaryView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
primaryView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
primaryView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
primaryView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
primaryView.layoutTitle()
}
}
PrimaryView
class - original, except added colors and bodyView properties
class MyPrimaryView: UIView {
var mainScroll = UIScrollView()
var mainStack: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
return stackView
} ()
var titleTextView: UITextView = {
let textView = UITextView()
textView.font = UIFont.systemFont(ofSize: 36)
textView.isScrollEnabled = false
textView.isEditable = false
return textView
}()
var mainActionButtonArea = UIView()
var mainActionButton: UIButton = {
let button = UIButton()
button.setImage(UIImage(systemName: "square.and.pencil.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 36)), for: .normal)
return button
}()
var bodyView = UIStackView()
init() {
super.init(frame: .zero)
setUpMainStack()
// set background colors to make it easy to see framing
self.backgroundColor = .cyan
mainScroll.backgroundColor = .systemBlue
mainStack.backgroundColor = .systemYellow
titleTextView.backgroundColor = .green
mainActionButtonArea.backgroundColor = .red.withAlphaComponent(0.75)
mainActionButton.backgroundColor = .white
bodyView.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
// let's give the bodyView a height constraint of 180
bodyView.heightAnchor.constraint(equalToConstant: 180.0).isActive = true
// and add a label to identify it
let v = UILabel()
v.font = .italicSystemFont(ofSize: 24.0)
v.text = "Body View"
bodyView.constrain(v, padding: 12.0, except: [.trailing, .bottom])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setUpMainStack() {
// Constrain scrollview to view
self.constrain(mainScroll, padding: 30)
// Constrain UIStackView to scroll view
mainScroll.constrain(mainStack)
mainScroll.widthAnchor.constraint(equalTo: mainStack.widthAnchor).isActive = true
// Add child views
mainStack.addArrangedSubview(titleTextView)
mainStack.addArrangedSubview(bodyView)
titleTextView.text = "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19"
// Add action button area + action button
mainScroll.constrain(mainActionButtonArea, except: [.leading, .bottom])
mainActionButtonArea.constrain(mainActionButton, padding: 10)
mainActionButton.heightAnchor.constraint(equalTo: mainActionButton.widthAnchor).isActive = true
}
func layoutTitle() {
// This adds the exception path; needs to be called after subviews are already laid out
let buttonPath = UIBezierPath(rect: mainActionButtonArea.frame)
titleTextView.textContainer.exclusionPaths = [buttonPath]
}
}
and it looks like this:
As you pointed out, the "17 18 19" line is missing.
So, let's force the text view to update its layout when we set the exclusion path. The only change will be a few lines added here:
func layoutTitle() {
// This adds the exception path; needs to be called after subviews are already laid out
let buttonPath = UIBezierPath(rect: mainActionButtonArea.frame)
titleTextView.textContainer.exclusionPaths = [buttonPath]
// "re-set" the text view's text and force a layout pass
let str: String = titleTextView.text ?? ""
titleTextView.text = ""
titleTextView.text = str
titleTextView.setNeedsLayout()
titleTextView.layoutIfNeeded()
}
Now when we run it, we get:
and there's no need to create a temporary label with sizing calculations that may, or may not, be reliable and flexible.
As a side note, to get this to "play well" with dynamic size changes - such as device rotation - it's still got a bit of work to do.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论