Is possible multiple section in uicollectionview Pinterest Layout. Multiple section is working in the Pinterest Layout class. I have tried this using Pinterest Layout class. Please help.
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 {
return 4
}else {
return 20
}
}
Related
I have UIStackView in vertical mode filled with UIButtons. I have dynamic screen resize and if all buttons in stack view have height under some threshold, I want to hide them. How to achive this automatically?
I have tried to extend UIButton and add:
override func layoutSubviews() {
super.layoutSubviews()
self.isHidden = (self.frame.height < 20)
}
which works, but once the button is hidden it will never re-appear and layoutSubviews is never called back (even if the height should be again larger).
It's not clear what all you are doing, or why you say it would be problematic to set the buttons' .alpha property, but here are two approaches, both using a UIStackView subclass and handling the show/hide in layoutSubviews().
1: calculate what the button heights will be and set .isHidden property:
class MyStackView: UIStackView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
axis = .vertical
distribution = .fillEqually
spacing = 8
}
override func layoutSubviews() {
super.layoutSubviews()
// approach 1
// setting .isHidden
let numViews = arrangedSubviews.count
let numSpaces = numViews - 1
let h = (bounds.height - (spacing * CGFloat(numSpaces))) / CGFloat(numViews)
let bHide = h < 20
arrangedSubviews.forEach { v in
v.isHidden = bHide
}
}
}
set .isHidden property based on what the button heights are (much simpler):
class MyStackView: UIStackView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
axis = .vertical
distribution = .fillEqually
spacing = 8
}
override func layoutSubviews() {
super.layoutSubviews()
// approach 2
// setting .alpha
arrangedSubviews.forEach { v in
v.alpha = v.frame.height < 20 ? 0.0 : 1.0
}
}
}
And here's a sample controller to see it in use. Tapping anywhere will toggle the height of the stack view between 300 and 100 (buttons will have less-than 20-pts height at 100):
class ConditionalStackViewController: UIViewController {
let stackView: MyStackView = {
let v = MyStackView()
// so we can see the stack view frame
v.backgroundColor = .systemYellow
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var stackHeight: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
for i in 1...6 {
let b = UIButton()
b.setTitle("Button \(i)", for: [])
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.backgroundColor = .red
stackView.addArrangedSubview(b)
}
view.addSubview(stackView)
let g = view.safeAreaLayoutGuide
stackHeight = stackView.heightAnchor.constraint(equalToConstant: 300.0)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
stackView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stackView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stackHeight,
])
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
view.addGestureRecognizer(t)
}
#objc func gotTap(_ g: UITapGestureRecognizer) -> Void {
stackHeight.constant = stackHeight.constant == 300 ? 100 : 300
}
}
How can I manually decide the next focused index path for my collection view on tvOS?
My use case is that I have a collection view with dynamic cells that scrolls in all directions, and sometimes when a cell in one section spans across several cells in the next one, I want to be able to manually decide what cell gets focused next.
The default behaviour seems to be that the collection view will select the cell in the middle of the previous one.
For example, the green cell is currently focused. When I navigate down, the collection view wants to focus the red cell, but I want the blue cell to be focused next.
My initial idea was to implement the collectionView(_:shouldUpdateFocusIn:) delegate function, and return false + trigger a focus update, when the collection view selects the "wrong" index path.
However, for some reason, shouldUpdateFocusIn gets called several times when I return false from it and causes a visible lag.
func collectionView(_ collectionView: UICollectionView, shouldUpdateFocusIn context: UICollectionViewFocusUpdateContext) -> Bool {
if let nextIndexPath = shouldFocusCell(context.focusHeading, (context.previouslyFocusedIndexPath, context.nextFocusedIndexPath)) {
// The collection view did not select the index path of the cell we want to focus next.
// Save the correct index path and trigger a focus update.
lastFocusChange = (lastFocusChange.next, nextIndexPath)
setNeedsFocusUpdate()
// Using updateFocusIfNeeded() results in the following warning:
// WARNING: Calling updateFocusIfNeeded while a focus update is in progress. This call will be ignored.
return false
}
return true
}
Next idea was to do the same thing in collectionView(_:didUpdateFocusIn:with:), but in this case we only update the focus after it has already moved to the "wrong" cell, so it becomes apparent to the user that the focus moves from the wrong cell to the correct one.
Not ideal either.
I'm using my own subclass of UICollectionViewLayout and UICollectionView, but I don't see anything I can override to be able to manually decide what index path to focus next when navigating up/down/left/right before shouldUpdateFocusIn is called.
Is there any way I can achieve this?
One possibility could be to use collectionView(_ collectionView:, canFocusItemAt:) to let your collectionView know if a given indexPath can receive the focus.
You can find here below a naive implementation of this concept. You will need to adjust the maths on it to your needs.
func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
guard let currentlyFocusedCellLayoutAttributes = collectionView.layoutAttributesForItem(at: focusedIndexPath) else { return false }
guard let cellAtGivenIndexPathLayoutAttributes = collectionView.layoutAttributesForItem(at: indexPath) else { return false }
let currentlyFocusedCellOriginX = currentlyFocusedCellLayoutAttributes.frame.origin.x
let currentlyFocusedCellOriginY = currentlyFocusedCellLayoutAttributes.frame.origin.y
let currentlyFocusedCellWidth = currentlyFocusedCellLayoutAttributes.frame.width
let cellAtGivenIndexPathOriginX = cellAtGivenIndexPathLayoutAttributes.frame.origin.x
let cellAtGivenIndexPathOriginY = cellAtGivenIndexPathLayoutAttributes.frame.origin.y
let cellAtGivenIndexPathWidth = cellAtGivenIndexPathLayoutAttributes.frame.width
let offsetX = collectionView.contentOffset.x
// Scrolling horizontally is always allowed
if currentlyFocusedCellOriginY == cellAtGivenIndexPathOriginY {
return true
}
// Scrolling vertically is only allowed to the first cell (blue cell in the screenshot)
if cellAtGivenIndexPathOriginX <= offsetX {
return true
}
return false
}
We can achieve by using UICollectionView delegate protocol method, pls find code snippt.
class ViewController: UIViewController {
#IBOutlet weak var sideMenuTableView: UITableView!
#IBOutlet weak var collectionView: UICollectionView!
let menueItem = ["Home", "Sports", "Movies", "Listen", "Play", "Game"]
let colors: [UIColor] = [.green, .blue, .purple, .orange, .yellow, .magenta, .brown, .black, .gray, .yellow, .green, .lightGray, .cyan, .magenta, .link, .blue, .yellow, .magenta, .brown, .black, .gray, .yellow, .green, .lightGray, .cyan, .magenta, .link, .blue]
var lastFocusedIndexPath: IndexPath?
override func viewDidLoad() {
super.viewDidLoad()
lastFocusedIndexPath = IndexPath(row: 2, section: 0)
initialConfiguration()
}
override var preferredFocusEnvironments : [UIFocusEnvironment] {
return [collectionView]
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateTableViewContentInset()
}
func updateTableViewContentInset() {
self.sideMenuTableView.contentInset = UIEdgeInsets(top: 0, left: -40, bottom: 0, right: 0)
}
func initialConfiguration() {
sideMenuTableView.register(UINib.init(nibName: "SideMenuTVCell", bundle: nil), forCellReuseIdentifier: "SideMenuTVCell")
sideMenuTableView.delegate = self
sideMenuTableView.dataSource = self
sideMenuTableView.backgroundColor = .yellow
sideMenuTableView.isHidden = false
collectionView.register(UINib.init(nibName: "ColorCVCell", bundle: nil), forCellWithReuseIdentifier: "ColorCVCell")
collectionView.delegate = self
collectionView.dataSource = self
collectionView.backgroundColor = .white
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return menueItem.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SideMenuTVCell", for: indexPath) as! SideMenuTVCell
cell.configureCell(string: menueItem[indexPath.row])
return cell
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ColorCVCell", for: indexPath) as! ColorCVCell
cell.backgroundColor = colors[indexPath.row]
cell.configureCell(color: colors[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
if let previousIndexPath = context.previouslyFocusedIndexPath,
let cell = collectionView.cellForItem(at: previousIndexPath) {
cell.contentView.layer.borderWidth = 0.0
cell.contentView.layer.shadowRadius = 0.0
cell.contentView.layer.shadowOpacity = 0
}
if let indexPath = context.nextFocusedIndexPath,
let cell = collectionView.cellForItem(at: indexPath) {
cell.contentView.layer.borderWidth = 8.0
cell.contentView.layer.borderColor = UIColor.black.cgColor
cell.contentView.layer.shadowColor = UIColor.black.cgColor
cell.contentView.layer.shadowRadius = 10.0
cell.contentView.layer.shadowOpacity = 0.9
cell.contentView.layer.shadowOffset = CGSize(width: 0, height: 0)
}
if let indexPath = context.previouslyFocusedIndexPath, let cell = collectionView.cellForItem(at: indexPath) {
UIView.animate(withDuration: 0.3) { () -> Void in
cell.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}
}
if let indexPath = context.nextFocusedIndexPath, let cell = collectionView.cellForItem(at: indexPath) {
UIView.animate(withDuration: 0.3) { () -> Void in
cell.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
}
}
}
func collectionView(_ collectionView: UICollectionView, shouldUpdateFocusIn context: UICollectionViewFocusUpdateContext) -> Bool {
if let previouslyFocusedIndexPath = context.previouslyFocusedIndexPath, let cell = collectionView.cellForItem(at: previouslyFocusedIndexPath) {
let collectionViewWidth = collectionView.frame.width
let cellWidth = cell.frame.width
let rowCount = Int(ceil(collectionViewWidth / cellWidth))
let remender = previouslyFocusedIndexPath.row % rowCount
let nextIndex = previouslyFocusedIndexPath.row - remender + rowCount
if let nextFocusedInndexPath = context.nextFocusedIndexPath {
if context.focusHeading == .down {
moveFocus(to: IndexPath(row: nextIndex, section: 0))
return true
}
}
}
return true
}
private func moveFocus(to indexPath: IndexPath) {
lastFocusedIndexPath = indexPath
print(collectionView.indexPathsForVisibleItems)
DispatchQueue.main.async {
self.setNeedsFocusUpdate()
self.updateFocusIfNeeded()
}
}
func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? {
return lastFocusedIndexPath
}
}
I am making a chat app and in this, I used jsqmessageviewcontroller for display chat but faced one issue in chat first item is attachment like image,pdf etc then time not display. if the first item is text then time display properly. So anyone has an idea then please help me.
For attachment used custom cells.
For timestamp display used below method:-
override func collectionView(_ collectionView: JSQMessagesCollectionView, attributedTextForCellTopLabelAt indexPath: IndexPath) -> NSAttributedString?
{
let message = self.messages[indexPath.item]
let messageVMObj = MessageViewModal(withMessage: message)
if(indexPath.item == 0){
print("indexPath.item == 0 called")
return messageVMObj.setDay()
} else {
let currentMessage = self.messages[indexPath.item]
let previousMessage = self.messages[indexPath.item - 1]
if DateExtension().compareDates(currentDate: currentMessage.date, previousDate: previousMessage.date)
{
return nil
}
else
{
return messageVMObj.setDay()
}
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout, heightForCellTopLabelAt indexPath: IndexPath) -> CGFloat {
let message = self.messages[indexPath.item]
let messageVMObj = MessageViewModal(withMessage: message)
//set height for group Msg Tag
if isGroup {
let msg = self.messages[indexPath.item]
if msg.gpMessageType != "" {
return 0.0
}
}
if(indexPath.item == 0){
return 20.0
} else {
let currentMessage = self.messages[indexPath.item]
let previousMessage = self.messages[indexPath.item - 1]
if DateExtension().compareDates(currentDate: currentMessage.date, previousDate: previousMessage.date)
{
return 0.0
}
else
{
return 20.0
}
}
}
So if any solution for that then please help me.
Thank you.
Either your implementation of custom cells is lacking parent super calls leading to no top label display or your isGroup variable is wrong somehow. Can you share your custom cell implementation ?
I want to implement drag and drop feature with UICollectionViewDragDelegate, UICollectionViewDropDelegate it works pretty well but I need to shrink the underneath cells while lifting one cell with drag behavior and shrink back to original size when user drop the cell back to the cells. But somehow I can't get a stable result with the code like below. The animation is weird and the behavior because unpredictable when trying to drag the cell on the edge of the screen. How can I make the result more predictable?
import UIKit
class ViewController: UIViewController {
#IBOutlet var collectionView: UICollectionView! {
didSet {
collectionView.delegate = self
collectionView.dataSource = self
collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
}
}
var items: [UIColor] = [.green, .blue, .brown, .cyan, .red, .magenta, .yellow, .systemPink]
var isShrink: Bool = false {
didSet {
//collectionView.collectionViewLayout.invalidateLayout()
collectionView.performBatchUpdates({}, completion: { _ in })
}
}
}
extension ViewController: UIGestureRecognizerDelegate {}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout _: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
isShrink ? CGSize(width: 60, height: 60) : CGSize(width: 80, height: 60)
}
}
class Cell: UICollectionViewCell {
var shrink: ((Bool) -> Void)?
required init?(coder: NSCoder) {
super.init(coder: coder)
let gesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressed(gesture:)))
addGestureRecognizer(gesture)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//shrink?(true)
print("touchesBegan")
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesEnd")
}
override init(frame: CGRect) {
super.init(frame: frame)
}
#objc func longPressed(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
print("began")
case .ended:
print("ended")
default:
print("default")
}
}
// weak var coordinator: DragCoordinator?
//
override func dragStateDidChange(_ dragState: UICollectionViewCell.DragState) {
super.dragStateDidChange(dragState)
switch dragState {
case .dragging:
shrink?(true)
case .lifting:
shrink?(true)
case .none:
shrink?(false)
#unknown default:
fatalError()
}
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
cell.shrink = { [weak self] isShrink in
self?.isShrink = isShrink
}
cell.backgroundColor = items[indexPath.row]
return cell
}
}
extension ViewController: UICollectionViewDragDelegate {
func collectionView(_ cv: UICollectionView,
itemsForBeginning _: UIDragSession,
at indexPath: IndexPath) -> [UIDragItem] {
[.init(itemProvider: .init(object: "\(indexPath.row)" as NSString))]
}
}
extension ViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView,
performDropWith coordinator: UICollectionViewDropCoordinator) {
guard let destinationIndexPath = coordinator.destinationIndexPath else {
return
}
let item = coordinator.items[0]
switch coordinator.proposal.operation {
case .move:
if let sourceIndexPath = item.sourceIndexPath {
collectionView.performBatchUpdates({
let item = items[sourceIndexPath.row]
items.remove(at: sourceIndexPath.row)
items.insert(item, at: destinationIndexPath.row)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
})
}
coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
default:
return
}
}
func collectionView(_: UICollectionView,
dropSessionDidUpdate session: UIDropSession,
withDestinationIndexPath _: IndexPath?) -> UICollectionViewDropProposal {
guard session.localDragSession != nil else {
return .init(operation: .copy, intent: .insertAtDestinationIndexPath)
}
guard session.items.count == 1 else {
return .init(operation: .cancel)
}
return .init(operation: .move, intent: .insertAtDestinationIndexPath)
}
}
I am having a lot of problems trying to get a couple of variables from one View Controller to the next. How can I do it properly?
Here's my code below. This is the view controller where I want to be able to send the variables RedScoreW and BlueScoreW to the next window. I am asking on HOW TO DO THIS using SWIFT language and specially for WATCHOS apps.
class InterfaceController2: WKInterfaceController {
var RedScoreW = 0
var BlueScoreW = 0
#IBOutlet var WatchRedScoreLabel: WKInterfaceLabel!
#IBOutlet var WatchBlueScoreLabel: WKInterfaceLabel!
#IBAction func RedScorePlus() {
if RedScoreW == 999 {
RedScoreW = 0
WatchRedScoreLabel.setText("0")
}else {
RedScoreW += 1
WatchRedScoreLabel.setText(String(RedScoreW))
}
}
#IBAction func RedScoreMinus() {
if RedScoreW == 0 {
RedScoreW = 999
WatchRedScoreLabel.setText("999")
}
else {
RedScoreW -= 1
WatchRedScoreLabel.setText(String(RedScoreW))
}
}
#IBAction func BlueScorePlus() {
if BlueScoreW == 999 {
BlueScoreW = 0
WatchBlueScoreLabel.setText("0")
} else{
BlueScoreW += 1
WatchBlueScoreLabel.setText(String(BlueScoreW))
}
}
#IBAction func BlueScoreMinus() {
if BlueScoreW == 0 {
BlueScoreW = 999
WatchBlueScoreLabel.setText("999")
}
else {
BlueScoreW -= 1
WatchBlueScoreLabel.setText(String(BlueScoreW))
}
}
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
WatchRedScoreLabel.setText(String(RedScoreW))
WatchBlueScoreLabel.setText(String(BlueScoreW))
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
}
And this is the Destination View Controller where I want to be able to use RedScoreW and BlueScoreW variables.
class InterfaceController3: WKInterfaceController {
#IBOutlet var finalRedScoreLabel: WKInterfaceLabel!
#IBOutlet var finalBlueScoreLabel: WKInterfaceLabel!
#IBAction func DoneAndResetButton() {
self.popToRootController()
}
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Configure interface objects here.
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
}
* EDIT *
I am trying to do it this way, this is the code where I send it, check:
#IBAction func FinishButtonPushVariables() {
arrayofScores[0] = RedScoreW
arrayofScores[1] = BlueScoreW
pushControllerWithName("LastScreen", context: arrayofScores)
}
And this is where I receive it... and it doesn't work. LOL
#IBOutlet var finalRedScoreLabel: WKInterfaceLabel!
#IBOutlet var finalBlueScoreLabel: WKInterfaceLabel!
#IBAction func DoneAndResetButton() {
self.popToRootController()
}
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
let finalarrayofScores = context as? InterfaceController2
finalBlueScoreLabel.setText(String(finalarrayofScores!.arrayofScores[1]))
finalRedScoreLabel.setText(String(finalarrayofScores!.arrayofScores[0]))
// Configure interface objects here.
}
In iOS apps, we use prepareForSegue to do this. On watchOS apps, we use contextForSegueWithIdentifier to pass a context from one interfaceController to another.
Here is a link to the class reference that will detail more about this. But here are the basics:
There are two different methods that can be used. One is for going from one interface controller to another:
func contextForSegueWithIdentifier(_ segueIdentifier: String) -> AnyObject?
The other is for going from a one interface controller to another when a row in a table is tapped:
func contextForSegueWithIdentifier(_ segueIdentifier: String, inTable table: WKInterfaceTable, rowIndex rowIndex: Int) -> AnyObject?
So one of these two methods will go in the interfaceController that is sending the context, and you will receive that context in the awakeWithContext method of the receiving interfaceController.
Here is a link to a tutorial that will show an application of this process.
EDIT
Here is a specific solution to your problem.
In the interface controller where you send it, put this code:
override func contextForSegueWithIdentifier(segueIdentifier: String) -> AnyObject? {
arrayofScores[0] = RedScoreW
arrayofScores[1] = BlueScoreW
return arrayOfScores
}
Then in your destination interface controller, put this code:
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
let finalArrayOfScores = context as? [Int]
if let f = finalArrayOfScores {
finalBlueScoreLabel.setText(String(f[1]))
finalRedScoreLabel.setText(String(f[0]))
}
}
You need to set up variables to hold your variable first.
class YourSecondViewController: UIViewController {
var yourVariable:Double?
}
Then have your button trigger your custom segue. Use your variable as the argument for sender.
class YourFirstViewController: UIViewController {
#IBAction func buttonTapped(sender: AnyObject) {
self.performSegueWithIdentifier("segue", sender: yourVariable)
}
}
Then pass the sender data by overriding the prepareForSegue method:
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if (segue.identifier = "segue") {
let secondViewController = segue.destinationViewController as YourSecondViewController
let yourVariable = sender as Double
secondViewController.duration = yourVariable
}
}
I guess your problem is that you are passing an array to the context and you cast it as WKIntefaceController.
Try replacing this line
let finalarrayofScores = context as? InterfaceController2
by
let finalarrayofScores = context as? [Int]